Open-source lifetime inference/verification code.

PiperOrigin-RevId: 450954978
diff --git a/lifetime_analysis/test/BUILD b/lifetime_analysis/test/BUILD
new file mode 100644
index 0000000..c0c0cab
--- /dev/null
+++ b/lifetime_analysis/test/BUILD
@@ -0,0 +1,188 @@
+# Test utilities and tests for lifetime_analysis.
+
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "lifetime_analysis_test",
+    testonly = 1,
+    srcs = ["lifetime_analysis_test.cc"],
+    hdrs = ["lifetime_analysis_test.h"],
+    deps = [
+        "//lifetime_analysis:analyze",
+        "//lifetime_annotations/test:named_func_lifetimes",
+        "//lifetime_annotations/test:run_on_code",
+        "@absl//absl/container:flat_hash_map",
+        "@com_google_googletest//:gtest",
+        ],
+)
+
+cc_test(
+    name = "builtin",
+    srcs = ["builtin.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "lifetime_params",
+    srcs = ["lifetime_params.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "virtual_functions",
+    srcs = ["virtual_functions.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "casts",
+    srcs = ["casts.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "initializers",
+    srcs = ["initializers.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "recursion",
+    srcs = ["recursion.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "function_templates",
+    srcs = ["function_templates.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "function_calls",
+    srcs = ["function_calls.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "execution_order",
+    srcs = ["execution_order.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "control_flow",
+    srcs = ["control_flow.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "basic",
+    srcs = ["basic.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "static_lifetime",
+    srcs = ["static_lifetime.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "arrays",
+    srcs = ["arrays.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "records",
+    srcs = ["records.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "inheritance",
+    srcs = ["inheritance.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "class_templates",
+    srcs = ["class_templates.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "initialization",
+    srcs = ["initialization.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "expr",
+    srcs = ["expr.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
+
+cc_test(
+    name = "defaulted_functions",
+    srcs = ["defaulted_functions.cc"],
+    deps = [
+        ":lifetime_analysis_test",
+        "@com_google_googletest//:gtest_main",
+    ],
+)
diff --git a/lifetime_analysis/test/arrays.cc b/lifetime_analysis/test/arrays.cc
new file mode 100644
index 0000000..dae88a9
--- /dev/null
+++ b/lifetime_analysis/test/arrays.cc
@@ -0,0 +1,112 @@
+// 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 involving arrays.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, ArrayOfInts) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target() {
+      int x[] = {0};
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ArrayMergesLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target(int** array, int* p, int* q) {
+      array[0] = p;
+      array[1] = q;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ArrayOfStructsMergesLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* i;
+    };
+    void target(S** array, S* p, S* q) {
+      array[0] = p;
+      array[1] = q;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b, c), (a, b), (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleArray) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, unsigned x) {
+      int* v[2];
+      v[0] = a;
+      v[1] = b;
+      return v[x & 1];
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleArrayInit) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, unsigned x) {
+      int* v[2] = {a, b};
+      return v[x & 1];
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleArrayInitConstExprSubscriptIndex) {
+  // There is a potential to track the lifetime of each array element
+  // separately, when the array's size and subscript indices are known
+  // statically. But is hard-to-impossible to do for all arrays. We treat an
+  // array as a single object as a result, and merge the points-to sets of all
+  // its elements.
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b) {
+      int* v[2] = {a, b};
+      return v[0];
+    }
+  )"),
+              LifetimesAre({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleArrayPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, unsigned x) {
+      int* v[2];
+      *v = a;
+      *(v + 1) = b;
+      return *(v + (x & 1));
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleArrayFnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, int** c, unsigned x) {
+      *c = a;
+      *(c + 1) = b;
+      return *(c + (x & 1));
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, (a, b), () -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/basic.cc b/lifetime_analysis/test/basic.cc
new file mode 100644
index 0000000..d442510
--- /dev/null
+++ b/lifetime_analysis/test/basic.cc
@@ -0,0 +1,462 @@
+// 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.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, CompilationError) {
+  // Check that we don't analyze code that doesn't compile.
+  // This is a regression test -- we actually used to produce the lifetimes
+  // "a -> a" for this test.
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a) {
+      undefined(&a);
+      return a;
+    }
+  )"),
+              LifetimesAre({{"", "Compilation error -- see log for details"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, CompilationErrorFallback) {
+  // Allow analysis of broken code to check that our fallback for detecting
+  // expressions containing errors works.
+  AnalyzeBrokenCode();
+
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    int* target(int* a) {
+      undefined(&a);
+      return a;
+    }
+  )"),
+      LifetimesAre(
+          {{"target", "ERROR: encountered an expression containing errors"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, CompilationErrorFromWerrorDoesNotPreventAnalysis) {
+  // Warnings upgraded through -Werror should not prevent analysis.
+  EXPECT_THAT(GetLifetimes(R"(
+#pragma clang diagnostic push
+#pragma clang diagnostic error "-Wunused-variable"
+    int* target(int* a) {
+      int i = 0;
+      return a;
+    }
+#pragma clang diagnostic pop
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, NoLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target() {
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, NoLifetimesArithmetic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int target(int a, int b) {
+      return (a + b) - (-b) * a;
+    }
+  )"),
+              LifetimesAre({{"target", "(), ()"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, PointerToMemberDoesNotGetLifetime) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {};
+    void target(S* s, int S::*ptr_to_member) {}
+  )"),
+              LifetimesAre({{"target", "a, ()"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, UnconstrainedParameter) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target(int* a) {
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a) {
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentPtrInitList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a) {
+      return { a };
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentRef) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target(int& a) {
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnFirstArgumentPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b) {
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnFirstArgumentRef) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target(int& a, int& b) {
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnRefFromPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target(int* a) {
+      return *a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnPtrFromRef) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int& a) {
+      return &a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnDereferencedArgument) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int** a) {
+      return *a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalViaPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target() {
+      int a = 42;
+      return &a;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalViaRef) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target() {
+      int a = 42;
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStaticViaPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target() {
+      static int a = 42;
+      return &a;
+    }
+  )"),
+              LifetimesAre({{"target", "-> static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StringLiteral) {
+  EXPECT_THAT(GetLifetimes(R"(
+    const char* target() {
+      return "this is a string literal";
+    }
+  )"),
+              LifetimesAre({{"target", "-> static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, OutParameter) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target(int& a) {
+      a = 42;
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, AssigningToPtrParamDoesNotChangeLifetime) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void target(int* p) {
+      int a = 42;
+      p = &a;
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, PtrInitializationTransfersLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int* p2 = p;
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, PtrAssignmentTransfersLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int* p2;
+      p2 = p;
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, RefInitializationTransfersLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target(int& r) {
+      int& r2 = r;
+      return r2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, RefAssignmentDoesNotTransferLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& target(int& r) {
+      int a = 42;
+      int& r2 = a;
+      r2 = r;
+      return r2;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalSneaky_Initialization) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* arg1) {
+      // Initialization should be aware that outer pointer is invariant in its
+      // type.
+      int** pp = &arg1;
+      int local = 42;
+      *pp = &local;
+      return arg1;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalSneaky_Assignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* arg1) {
+      // Assignment should be aware that outer pointer is invariant in its type.
+      int** pp;
+      pp = &arg1;
+      int local = 42;
+      *pp = &local;
+      return arg1;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalSneaky2_Initialization) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* arg1) {
+      // Initialization should be aware that outer pointer is invariant in its
+      // type.
+      int** pp = &arg1;
+      int local = 42;
+      arg1 = &local;
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalSneaky2_Assignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* arg1) {
+      // Assignment should be aware that outer pointer is invariant in its type.
+      int** pp;
+      pp = &arg1;
+      int local = 42;
+      arg1 = &local;
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnLocalSneaky3_Initialization) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* arg1) {
+      int*& pp = arg1;
+      int local = 42;
+      arg1 = &local;
+      return pp;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SwapPointers) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void swap_ptr(int** pp1, int** pp2) {
+      int* tmp = *pp2;
+      *pp2 = *pp1;
+      *pp1 = tmp;
+    }
+  )"),
+              LifetimesAre({{"swap_ptr", "(a, b), (a, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DuplicatePointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void duplicate_ptr(int* from, int** to1, int** to2) {
+      *to1 = from;
+      *to2 = from;
+    }
+  )"),
+              LifetimesAre({{"duplicate_ptr", "a, (a, b), (a, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, Aliasing) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int** a, int** b, int* c) {
+      *a = c;
+      return *b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), (c, d), a -> c"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, IncompleteType) {
+  // Test that we can handle pointers to incomplete types.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S;
+    S* target(S* s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_IncompleteTypeTemplate) {
+  // TODO(mboehme): Disabled because it returns the wrong lifetimes.
+  // S<int*> is never instantiated because we only deal with pointers to it,
+  // so it's an incomplete type.
+  //
+  // We can handle incomplete types in principle, but in this case,  because
+  // we don't create any pointees for the fields of `S<int*>`, we will produce
+  // these incorrect lifetimes:
+  //   (a, b) -> (c, b)
+  // Even more strangely, the lifetimes we infer change (to the correct ones)
+  // once we happen to instantiate S<int*> somewhere else in the same
+  // translation unit.
+  //
+  // I'm not sure how best to solve this. We could simply force instantiation
+  // of all uninstantiated templates we see, but I believe this might change the
+  // semantics of the program in subtle ways.
+  //
+  // The better alternative seems to be: If we're unifying lifetimes of an
+  // object that is of an instantiated class template type, unify the lifetimes
+  // of its template arguments too. This can be overly restrictive -- think of a
+  // class template that doesn't actually use its template arguments in any of
+  // its fields, e.g. `template <class T> struct S {};`. However, it seems to be
+  // the only option that produces consistent results without requiring us to
+  // instantiate class templates that could otherwise be used as incomplete
+  // types.
+  EXPECT_THAT(GetLifetimes(R"(
+    template <class T>
+    struct S {
+      T t;
+    };
+
+    S<int*>* target(S<int*>* s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, UndefinedFunction_NoLifetimeElision) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    int* f(int* a);
+    int* target(int* a) {
+      return f(a);
+    }
+  )"),
+      LifetimesAre({{"f", "ERROR: Lifetime elision not enabled for 'f'"},
+                    {"target",
+                     "ERROR: No lifetimes for callee 'f': Lifetime elision not "
+                     "enabled for 'f'"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, UndefinedFunction_LifetimeElision) {
+  EXPECT_THAT(GetLifetimes(R"(
+    #pragma clang lifetime_elision
+    int* f(int* a);
+    int* target(int* a) {
+      return f(a);
+    }
+  )"),
+              LifetimesAre({{"f", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ForwardDeclaration) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int* a);
+    int* target(int* a) {
+      return f(a);
+    }
+    int* f(int* a) {
+      return a;
+    }
+  )"),
+              LifetimesAre({{"f", "a -> a"}, {"target", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/builtin.cc b/lifetime_analysis/test/builtin.cc
new file mode 100644
index 0000000..945aedd
--- /dev/null
+++ b/lifetime_analysis/test/builtin.cc
@@ -0,0 +1,122 @@
+// 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 involving builtins.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, ReturnPtrFromRefAddressOf) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int& a) {
+      return __builtin_addressof(a);
+    }
+  )"),
+              LifetimesContain({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnDoublePtrFromRefAddressOf) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int** target(int*& a) {
+      return __builtin_addressof(a);
+    }
+  )"),
+              LifetimesContain({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, BuiltinNoLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int target(int a) {
+      return __builtin_labs(a);
+    }
+  )"),
+              LifetimesContain({{"target", "()"}}));
+}
+
+// TODO(veluca): add tests for the strto* functions.
+
+TEST_F(LifetimeAnalysisTest, BuiltinMemStrChr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void* memchr(void* a, int val, int num) {
+      return __builtin_memchr(a, val, num);
+    }
+    const char* strchr(const char* a, int val) {
+      return __builtin_strchr(a, val);
+    }
+    const char* strrchr(const char* a, int val) {
+      return __builtin_strrchr(a, val);
+    }
+  )"),
+              LifetimesContain({
+                  {"memchr", "a, (), () -> a"},
+                  {"strchr", "a, () -> a"},
+                  {"strrchr", "a, () -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, BuiltinStrProcessing) {
+  EXPECT_THAT(GetLifetimes(R"(
+    const char* strstr(const char* a, const char* b) {
+      return __builtin_strstr(a, b);
+    }
+    const char* strpbrk(const char* a, const char* b) {
+      return __builtin_strpbrk(a, b);
+    }
+  )"),
+              LifetimesContain({
+                  {"strstr", "a, b -> a"},
+                  {"strpbrk", "a, b -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, BuiltinForward) {
+  EXPECT_THAT(GetLifetimes(R"(
+    namespace std {
+      // This is simplified from the actual definition of forward(), but it's
+      // all we need for this test.
+      template<class T>
+      T&& forward(T& t) noexcept {
+        return static_cast<T&&>(t);
+      }
+    }
+    int* target(int* a) {
+      return std::forward(a);
+    }
+  )"),
+              LifetimesContain({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, BuiltinMove) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    namespace std {
+      // This is simplified from the actual definition of move(), but it's all
+      // we need for this test.
+      template<class T>
+      T&& move(T&& t) noexcept {
+        return static_cast<T&&>(t);
+      }
+    }
+    int* move_int_ptr(int* a) {
+      return std::move(a);
+    }
+    template <class T, class U> struct S { T t; U u; };
+    S<int**, int*> move_template(S<int**, int*> s) {
+      return std::move(s);
+    }
+  )"),
+      LifetimesContain({{"move_int_ptr", "a -> a"},
+                        {"move_template", "(<a, b, c>) -> (<a, b, c>)"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/casts.cc b/lifetime_analysis/test/casts.cc
new file mode 100644
index 0000000..bfc1c20
--- /dev/null
+++ b/lifetime_analysis/test/casts.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 involving casts.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, DISABLED_StaticCast) {
+  // TODO(veluca): the `Object` we create for the base struct does not know
+  // about the derived struct, so this test will fail when trying to access the
+  // base on the object of the derived class. See also DynamicCastAccessField.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct Base {
+      virtual bool is_derived() { return false; }
+    };
+    struct Derived : public Base {
+      bool is_derived() override {
+        return true;
+      }
+    };
+    Derived* test_static_cast_ptr(Base* base, Derived* derived) {
+      if (base->is_derived()) {
+        return static_cast<Derived*>(base);
+      }
+      return derived;
+    }
+    Derived& test_static_cast_ref(Base& base, Derived& derived) {
+      if (base.is_derived()) {
+        return static_cast<Derived&>(base);
+      }
+      return derived;
+    }
+  )"),
+              LifetimesContain({{"test_static_cast_ptr", "a, a -> a"},
+                                {"test_static_cast_ref", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_DynamicCastWithFnCall) {
+  // TODO(veluca): the `Object` we create for the base struct does not know
+  // about the derived struct, so this test will fail when trying to access the
+  // base on the object of the derived class. See also DynamicCastAccessField.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct Base {
+      virtual bool is_derived() { return false; }
+    };
+    struct Derived : public Base {
+      bool is_derived() override {
+        return true;
+      }
+    };
+    Derived* test_dynamic_cast_ptr(Base* base, Derived* derived) {
+      if (Derived* derived_from_base = dynamic_cast<Derived*>(base)) {
+        return derived_from_base;
+      }
+      return derived;
+    }
+    Derived& test_dynamic_cast_ref(Base& base, Derived& derived) {
+      // We don't have support for exceptions enabled, so we can't use
+      // dynamic_cast unconditionally and check whether it succeeded or failed
+      // by catching std::bad_cast. Instead, we call is_derived() like we do in
+      // StaticCast test above. This makes the dynamic_cast somewhat pointless,
+      // but at least we can test that we do propagate the points-to set through
+      // it correctly.
+      if (base.is_derived()) {
+        return dynamic_cast<Derived&>(base);
+      }
+      return derived;
+    }
+    // Also test that we handle function calls to test_dynamic_cast_...()
+    // correctly.  Our logic for function calls should realize that
+    // test_dynamic_cast_...() may return not just `derived` but also `base` and
+    // that therefore all three lifetimes should be the same.
+    Derived* call_dynamic_cast_ptr(Base* base, Derived* derived) {
+      return test_dynamic_cast_ptr(base, derived);
+    }
+    Derived& call_dynamic_cast_ref(Base& base, Derived& derived) {
+      return test_dynamic_cast_ref(base, derived);
+    }
+  )"),
+              LifetimesContain({{"test_dynamic_cast_ptr", "a, a -> a"},
+                                {"test_dynamic_cast_ref", "a, a -> a"},
+                                {"call_dynamic_cast_ptr", "a, a -> a"},
+                                {"call_dynamic_cast_ref", "a, a -> a"}}));
+}
+
+// TODO(mboehme): This test currently fails when trying to access `derived->a`
+// because it can't find the field. This is because we set up the object as a
+// `Base` and only gave it the fields that are present on `Base`.
+// There are several ways we could resolve this:
+// a) When setting up the object initially, proactively give it the fields of
+//    all transitive derived classes. This can, however, be very costly if the
+//    object type is the base class of a large object hierarchy.
+// b) When the object becomes accessible through a pointer to the derived class,
+//    add all of the fields of that derived class if they aren't present yet.
+// c) When we perform a field access, add the field if it isn't present yet.
+// Of these, c) may be the easiest to implement, and it also avoids
+// speculatively adding fields to the object that may never be accessed.
+TEST_F(LifetimeAnalysisTest, DISABLED_DynamicCastAccessField) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct Base {
+      virtual ~Base() {}
+    };
+    struct Derived : public Base {
+      int* p;
+    };
+    Derived* DerivedFromBase(Base* base) {
+      return dynamic_cast<Derived*>(base);
+    }
+    int* target(Base* base) {
+      if (auto* derived = DerivedFromBase(base)) {
+        return derived->p;
+      }
+      return nullptr;
+    }
+  )"),
+      LifetimesContain({{"DerivedFromBase", "a -> a"}, {"target", "a -> a"}}));
+}
+
+// TODO(mboehme): This example demonstrates an issue related to field access on
+// derived classes that may be hard to overcome in a principled way. In a
+// multi-TU setting, neither the definition of these functions nor that of the
+// class Derived need be visible within the TU that contains target().
+// Currently, this example fails because SetFieldIfPresent() and
+// GetFieldIfPresent() cannot access Derived::p. But even when this is resolved,
+// we face these issues:
+// - The call to SetFieldIfPresent() is a no-op with respect to the points-to
+//   map. Even though `base` and `p` share the same lifetime, the logic for
+//   performing function calls doesn't see any object of type `int*` that could
+//   be modified by the callee.
+// - There is no existing object for GetFieldIfPresent() to return.
+// We could fix the second issue by creating a new object and giving it the same
+// lifetime as `base` (which we know to do because of the signature of
+// GetFieldIfPresent()). However, we would still infer incorrect lifetimes of
+// "a, b -> b" for target() because we would not understand that `p` potentially
+// gets propagated to the return value of target().
+// The alternative function call algorithm that veluca@ is working on might
+// resolve this issue and infer correct lifetimes in this case.
+TEST_F(LifetimeAnalysisTest, DISABLED_DynamicCastFieldAccessBehindFnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct Base {
+      virtual ~Base() {}
+    };
+    struct Derived : public Base {
+      int* p;
+    };
+    void SetFieldIfPresent(Base* base, int* p) {
+      if (auto* derived = dynamic_cast<Derived*>(base)) {
+        derived->p = p;
+      }
+    }
+    int* GetFieldIfPresent(Base* base) {
+      if (auto* derived = dynamic_cast<Derived*>(base)) {
+        return derived->p;
+      }
+      return nullptr;
+    }
+    int* target(Base* base, int* p) {
+      SetFieldIfPresent(base, p);
+      return GetFieldIfPresent(base);
+    }
+  )"),
+              LifetimesContain({{"SetFieldIfPresent", "a, a"},
+                                {"GetFieldIfPresent", "a -> a"},
+                                {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReinterpretCastPtr) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    double* target(int* p) {
+      return reinterpret_cast<double*>(p);
+    }
+  )"),
+      LifetimesAre({{"target", "ERROR: type-unsafe cast prevents analysis"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReinterpretCastRef) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    double& target(int& p) {
+      return reinterpret_cast<double&>(p);
+    }
+  )"),
+      LifetimesAre({{"target", "ERROR: type-unsafe cast prevents analysis"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, IntegralToPointerCast) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    // We want to avoid including <cstdint>, so just assume `long long` is big
+    // enough to hold a pointer.
+    int* target(long long i) {
+      return reinterpret_cast<int*>(i);
+    }
+  )"),
+      LifetimesAre({{"target", "ERROR: type-unsafe cast prevents analysis"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/class_templates.cc b/lifetime_analysis/test/class_templates.cc
new file mode 100644
index 0000000..4927f33
--- /dev/null
+++ b/lifetime_analysis/test/class_templates.cc
@@ -0,0 +1,631 @@
+// 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 involving class templates.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, StructTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    int* target(S<int*> s) {
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplatePtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    int* target(S<int*>* s) {
+      return s->a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateInnerDoubleUsage) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+      T b;
+    };
+    int* target(S<int**>* s) {
+      int l = 0;
+      *s->b = &l;
+      return *s->a;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local "
+                             "through parameter 's'"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTwoTemplateArguments) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T, typename U>
+    struct S {
+      T t;
+      U u;
+    };
+
+    int* return_t(S<int*, int*>& v) {
+      return v.t;
+    }
+
+    int* return_u(S<int*, int*>& v) {
+      return v.u;
+    }
+  )"),
+              LifetimesAre({{"return_t", "(<a, b>, c) -> a"},
+                            {"return_u", "(<a, b>, c) -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTwoTemplateArgumentsNestedClasses) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct Outer {
+      template <typename U>
+      struct Inner {
+        T t;
+        U u;
+      };
+    };
+
+    int* return_t(Outer<int*>::Inner<int*>& inner) {
+      return inner.t;
+    }
+
+    int* return_u(Outer<int*>::Inner<int*>& inner) {
+      return inner.u;
+    }
+  )"),
+              LifetimesAre({{"return_t", "(<a>::<b>, c) -> a"},
+                            {"return_u", "(<a>::<b>, c) -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTwoTemplateArgumentsConstructInner) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct Inner {
+      Inner (T a): a(a) {}
+      T a;
+    };
+    template <typename T, typename U>
+    struct Outer {
+      Outer(T a, U& b): a(a), b(b) {}
+      T a;
+      U b;
+    };
+    int* target(int* a, int* b) {
+      Inner<int*> is(b);
+      Outer<int*, Inner<int*>> s(a, is);
+      return s.b.a;
+    }
+  )"),
+              LifetimesContain({{"target", "a, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTwoTemplateArgumentsTernary) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T, typename U>
+    struct S {
+      T t;
+      U u;
+    };
+
+    int* f(S<int*, int*>& v) {
+      return *v.t < *v.u ? v.t : v.u;
+    }
+  )"),
+              LifetimesAre({{"f", "(<a, a>, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateLocalVariable) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    const int* target(S<int*> s) {
+      S<const int*> t;
+      t.a = s.a;
+      return t.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplatePointerToMember) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T, typename U>
+    struct S {
+      T a;
+      U b;
+    };
+    int** target(S<int*, int*>& s) {
+      return &s.b;
+    }
+  )"),
+              LifetimesAre({{"target", "(<a, b>, c) -> (b, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateWithPointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      T* a;
+    };
+    int** target(S<int*>& s) {
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"target", "(<a> [b], c) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateWithTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    int* target(S<S<int*>> s) {
+      return s.a.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateInnerTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct U {
+      T a;
+    };
+    template <typename T>
+    struct S {
+      U<T> a;
+    };
+    int* target(S<int*>* s) {
+      return s->a.a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_StructTemplateInnerTemplatePtr) {
+  // TODO(veluca): we don't correctly propagate lifetime arguments when creating
+  // template arguments for fields that use the template argument indirectly,
+  // such as behind a pointer or as template arguments to a struct passed as a
+  // template argument to the member.
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct U {
+      T a;
+    };
+    template <typename T>
+    struct S {
+      U<T*> a;
+    };
+    int* target(S<int*>* s) {
+      return *s->a.a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateSwapArguments) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T, typename U>
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      T a;
+      U b;
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      S<U, T>* next;
+    };
+    int* target(S<int*, int*>* s) {
+      return s->next->a;
+    }
+    int* target_swtwice(S<int*, int*>* s) {
+      return s->next->next->a;
+    }
+  )"),
+              LifetimesAre({{"target", "(<a, b> [c], d) -> b"},
+                            {"target_swtwice", "(<a, b> [c], d) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateMemberCall) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    // TODO(mboehme): The real `vector` doesn't have lifetime parameters, but
+    // we use these here as we don't have the ability to do `lifetime_cast`s
+    // yet.
+    struct [[clang::annotate("lifetime_params", "a")]] vector {
+      T& operator[](int i) { return a[i]; }
+      [[clang::annotate("member_lifetimes", "a")]]
+      T* a;
+    };
+
+    int* get(vector<int*>& v, int i) {
+      return v[i];
+    }
+  )"),
+      LifetimesAre({{"vector<int *>::operator[]", "(<a> [b], c): () -> (a, b)"},
+                    {"get", "(<a> [b], c), () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTwoTemplateArgumentsCall) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T, typename U>
+    struct S {
+      T t;
+      U u;
+    };
+
+    int* f(S<int*, int*>& v) {
+      return *v.t < *v.u ? v.t : v.u;
+    }
+
+    int* g(S<int*, int*>& v) {
+      return f(v);
+    }
+  )"),
+      LifetimesAre({{"f", "(<a, a>, b) -> a"}, {"g", "(<a, a>, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructNoTemplateInnerTemplate) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct X {
+      T field;
+    };
+
+    struct Y {
+     X<int*> field;
+    };
+
+    int* target_byref(Y& s) {
+      return s.field.field;
+    }
+
+    int* target_byvalue(Y s) {
+      return s.field.field;
+    }
+  )"),
+      LifetimesContain({{"target_byref", "a -> a"},
+                        {"target_byvalue",
+                         "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturn) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(S<int*>& s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnXvalue) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(S<int*> s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> take_by_ref(S<int*>& s) {
+      return s;
+    }
+    S<int*> take_by_value(S<int*> s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"take_by_ref", "(a, b) -> a"},
+                            {"take_by_value", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnLocal) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(int* a) {
+      int i = 42;
+      return { &i };
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructTemporaryConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T a) : a(a) {}
+      T a;
+    };
+    S<int*> ConstructorCastSyntax(int* a) {
+      return S(a);
+    }
+    S<int*> ConstructTemporarySyntax(int* a) {
+      return S{a};
+    }
+  )"),
+              LifetimesAre({{"S<int *>::S", "(a, b): a"},
+                            {"ConstructorCastSyntax", "a -> a"},
+                            {"ConstructTemporarySyntax", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructTemporaryInitializerList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> InitListExpr(int* a) {
+      return {a};
+    }
+    S<int*> CastWithInitListExpr(int* a) {
+      return S<int*>{a};
+    }
+  )"),
+              LifetimesAre({{"InitListExpr", "a -> a"},
+                            {"CastWithInitListExpr", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnUnionTemporaryInitializerList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    union S {
+      T a;
+    };
+    S<int*> target(int* a) {
+      return {a};
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_StructTemplateReturnPassByValue) {
+  // TODO(veluca): disabled because calling a function with a pass-by-value
+  // struct is not yet supported -- see TODO in TransferLifetimesForCall.
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> t(S<int*> s) {
+      return s;
+    }
+    S<int*> target(S<int*> s) {
+      return t(s);
+    }
+  )"),
+              LifetimesAre({{"t", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithTemplateArgs) {
+  EXPECT_THAT(GetLifetimes(R"(
+template <typename T, typename U>
+struct S {
+  T t;
+  U u;
+};
+
+int* target(S<int*, int*>* s, int* t, int* u) {
+  s->t = t;
+  s->u = u;
+  return s->t;
+}
+  )"),
+              // With template arguments, now the struct and its fields can
+              // have different lifetimes.
+              LifetimesAre({{"target", "(<a, b>, c), a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ExampleFromRFC) {
+  // This is an example from the lifetimes RFC.
+  EXPECT_THAT(GetLifetimes(R"(
+template <typename T>
+struct R {
+  R(T t) : t(t) {}
+  T t;
+};
+
+bool some_condition();
+
+template <typename T>
+struct S {
+  S(T a, T b) : r(some_condition() ? R(a) : R(b)) {}
+  R<T> r;
+};
+
+int* target(int* a, int* b) {
+  S<int*> s(a, b);
+  return s.r.t;
+}
+  )"),
+              LifetimesContain({{"R<int *>::R", "(a, b): a"},
+                                {"S<int *>::S", "(a, b): a, a"},
+                                {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VariadicTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <int idx, typename... Args> struct S {};
+    template <int idx, typename T, typename... Args>
+    struct S<idx, T, Args...> {
+      T t;
+      S<idx+1, Args...> nested;
+    };
+
+    template <typename... Args>
+    struct tuple: public S<0, Args...> {};
+
+    int* target(tuple<int*, int*>& s) {
+      return s.nested.t;
+    }
+  )"),
+              LifetimesAre({{"target", "(<a, b>, c) -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_VariadicTemplateConstructTrivial) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <int idx, typename... Args> struct S {};
+    template <int idx, typename T, typename... Args>
+    struct S<idx, T, Args...> {
+      T t;
+      S<idx+1, Args...> nested;
+    };
+
+    template <typename... Args>
+    struct tuple: public S<0, Args...> {};
+
+    void target(int* a, int* b) {
+      tuple<int*, int*> s = {a, b};
+    }
+  )"),
+              LifetimesAre({{"target", "a, b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VariadicTemplateConstruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename... Args> struct S { S() {} };
+    template <typename T, typename... Args>
+    struct S<T, Args...> {
+      T t;
+      S<Args...> nested;
+      S(T t, Args... args): t(t), nested(args...) {}
+    };
+
+    void target(int* a, int* b) {
+      S<int*, int*> s = {a, b};
+    }
+  )"),
+              LifetimesContain({{"target", "a, b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, NoexceptTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S() noexcept(isnoexcept<T>()) {}
+      template <typename U>
+      static constexpr bool isnoexcept() { return true; }
+    };
+
+    void f() {
+      S<int> s;
+    }
+  )"),
+              LifetimesContain({{"f", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TypeTemplateArgAfterNonType) {
+  // Minimized repro for a crash from b/228325046.
+  EXPECT_THAT(GetLifetimes(R"(
+    template<int _Idx, typename _Head>
+    struct _Head_base
+    {
+      constexpr _Head_base(_Head&& __h)
+        : _M_head_impl(__h) { }
+
+      _Head _M_head_impl;
+    };
+
+    void f() {
+      _Head_base<0, void*> head_base(nullptr);
+    }
+  )"),
+              LifetimesContain({{"f", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       TemplateContainingTypedefInstantiatedAnotherTemplate) {
+  // Minimized repro for a crash from b/228325046.
+  // The scenario that triggered the crash is:
+  // - We have a template (in this case `remove_reference`) containing a typedef
+  // - That typedef depends on a template parameter
+  // - We instantiate the template with an argument that is another template
+  // The bug was that we weren't desugaring the typedef and hence coming up with
+  // a different value for the depth of the template argument than
+  // TemplateTypeParmType::getDepth() uses.
+  EXPECT_THAT(GetLifetimes(R"(
+    namespace std {
+      template <typename T1, typename T2> struct pair {
+        T1 t1;
+        T2 t2;
+      };
+
+      template<typename _Tp>
+        struct remove_reference
+        { typedef _Tp   type; };
+
+      template<typename _Tp>
+        constexpr _Tp&&
+        forward(typename std::remove_reference<_Tp>::type& __t) noexcept
+        { return static_cast<_Tp&&>(__t); }
+    }
+
+    void f() {
+      std::pair<int, int> p;
+      std::forward<decltype(p)>(p);
+    }
+  )"),
+              LifetimesContain({{"f", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DISABLED_ReturnPointerToTemplate) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <class T> struct S { T t; };
+    S<int*>* target(S<int*>* s) {
+      return s;
+    }
+  )"),
+      // TODO(b/230456778): This currently erroneously returns (a, b) -> (c, b)
+      LifetimesAre({{"f", "(a, b) -> (a, b)"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/control_flow.cc b/lifetime_analysis/test/control_flow.cc
new file mode 100644
index 0000000..0b4f354
--- /dev/null
+++ b/lifetime_analysis/test/control_flow.cc
@@ -0,0 +1,232 @@
+// 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 control flow is taken into account correctly.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentWithControlFlow) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+  )"),
+              LifetimesAre({{"get_lesser_of", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnPtrArgumentWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b) {
+      return *a < *b? a : b;
+    }
+  )"),
+              LifetimesAre({{"get_lesser_of", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnRefArgumentWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int& get_lesser_of(int& a, int& b) {
+      return a < b? a : b;
+    }
+  )"),
+              LifetimesAre({{"get_lesser_of", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ControlFlowExceptionsWorkSometimes) {
+  // This test documents that we do understand the control flow resulting from
+  // exceptions in some limited circumstances. However, this is not true in the
+  // general case -- see the test ControlFlowExceptionsNotSupportedInGeneral --
+  // and it's a non-goal to add support for this.
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b) {
+      try {
+        throw 42;
+        return a;
+      } catch(...) {
+        return b;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "a, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ControlFlowExceptionsNotSupportedInGeneral) {
+  // This test documents that we do not in general treat the control flow
+  // resulting from exceptions correctly; changing this is a non-goal.
+  EXPECT_THAT(GetLifetimes(R"(
+    void may_throw() {
+      throw 42;
+    }
+    int* target(int* a, int* b) {
+      try {
+        may_throw();
+        return a;
+      } catch(...) {
+        return b;
+      }
+    }
+  )"),
+              LifetimesContain({{"target", "a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DoublePointerWithConditionalAssignment) {
+  // This is a regression test for a bug where we were not taking all
+  // substitutions into account in the return value lifetimes.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    int** target(int** pp1, int** pp2) {
+      if (**pp1 > **pp2) {
+        *pp1 = *pp2;
+      }
+      return pp2;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), (a, c) -> (a, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentWithControlFlowAndJoin) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b) {
+      int* p = a;
+      if (*a < *b) {
+        p = b;
+      }
+      return p;
+    }
+  )"),
+              LifetimesAre({{"get_lesser_of", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnArgumentWithUnnecessaryAssignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a, int* b) {
+      for (int i=0; i<*a; i++) {
+        p = a;
+        p = b;
+      }
+      return p;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceInControlFlow) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a) {
+      int local = 42;
+      int** pp = &a;
+      if (*a < *p) {
+        pp = &p;
+      }
+      p = &local;
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceEndOfBlock) {
+  // Make sure that the analysis handles statement ordering correctly.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a) {
+      int local = 42;
+      int** pp = &a;
+      int* b = a;
+      int* p = a;
+      if (*p < *a) {
+        p = b;
+        pp = &p;
+        b = &local;
+      }
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceSneaky) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a) {
+      int local = 42;
+      int** pp = &a;
+      int* b = a;
+      for (int i=0; i<*a; i++) {
+        p = b;
+        pp = &p;
+        b = &local;
+      }
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceSneakyParam) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a, int* c) {
+      int** pp = &a;
+      int* b = a;
+      for (int i=0; i<*a; i++) {
+        p = b;
+        pp = &p;
+        b = c;
+      }
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceAndOverwrite) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a, int* b) {
+      int** pp = &a;
+      if (*a < *p) {
+        pp = &p;
+      }
+      p = b;
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceTooStrict) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p, int* a) {
+      int** pp = &a;
+      if (*p < *a) {
+        p = a;
+        pp = &p;
+      }
+      return *pp;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a -> a"}}));
+  // TODO(mboehme): This result is too strict. This is because at the
+  // return statement, the analysis concludes that
+  // - pp may be pointing at either p or a, and
+  // - p may either still have its original value or it may be pointing at a
+  // The analysis doesn't "know" that the combination "p has original value and
+  // pp points at p" can never occur. It may be possible to solve this with path
+  // conditions -- IIUC, this is exactly what they are for.
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/defaulted_functions.cc b/lifetime_analysis/test/defaulted_functions.cc
new file mode 100644
index 0000000..c3b4046
--- /dev/null
+++ b/lifetime_analysis/test/defaulted_functions.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 that defaulted functions are analyzed correctly.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+class DefaultedFunctions : public LifetimeAnalysisTest {};
+
+TEST_F(DefaultedFunctions, DefaultConstrutor_NoRecordTypeFieldsNoBases) {
+  GetLifetimesOptions options;
+  options.include_implicit_methods = true;
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int i;
+    };
+    void target() {
+      S();
+    }
+
+  )",
+                           options),
+              // Test is successful if we can call the default constructor.
+              LifetimesAre({{"S::S", "a:"}, {"target", ""}}));
+}
+
+TEST_F(DefaultedFunctions, DefaultConstrutor_LifetimeParam) {
+  GetLifetimesOptions options;
+  options.include_implicit_methods = true;
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* p;
+    };
+    void target() {
+      S();
+    }
+
+  )",
+                           options),
+              LifetimesAre({{"S::S", "(a, b):"}, {"target", ""}}));
+}
+
+TEST_F(DefaultedFunctions, DefaultConstrutor_RecordTypeFields) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct S {};
+    struct T {
+      S s;
+    };
+    void f() {
+      T();
+    }
+  )"),
+      // TODO(b/230693710): This documents that defaulted default
+      // constructors on classes with record-type fields are currently
+      // not supported.
+      LifetimesAre({{"T::T", "ERROR: unsupported type of defaulted function"},
+                    {"f", "ERROR: No lifetimes for constructor T"}}));
+}
+
+TEST_F(DefaultedFunctions, DefaultConstrutor_BaseClass) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct S {};
+    struct T : public S {};
+    void f() {
+      T();
+    }
+  )"),
+      // TODO(b/230693710): This documents that defaulted default
+      // constructors on derived classes are currently not supported.
+      LifetimesAre({{"T::T", "ERROR: unsupported type of defaulted function"},
+                    {"f", "ERROR: No lifetimes for constructor T"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/execution_order.cc b/lifetime_analysis/test/execution_order.cc
new file mode 100644
index 0000000..a69a1a0
--- /dev/null
+++ b/lifetime_analysis/test/execution_order.cc
@@ -0,0 +1,107 @@
+// 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 execution order is taken into account correctly.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, OrderOfOperations_OrderOfExecution) {
+  // This is a regression test for a wrong result that was generated by the
+  // constraint-based approach.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int* p2 = p;
+      int local = 42;
+      p = &local;
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, OrderOfOperations_OrderOfExecution2) {
+  // This is a regression test for a wrong result that was generated by the
+  // constraint-based approach.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int **pp = &p;
+      int local = 42;
+      int* p2;
+      pp = &p2;
+      *pp = &local;
+      return p;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, OrderOfOperations_Assignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int* p2 = p;
+      int local = 42;
+      p2 = &local;
+      p2 = p;
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, TakeAReferenceLater) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* p) {
+      int* p2 = p;
+      int local = 42;
+      p = &local;
+      int** pp = &p;
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ChainedAssignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, int* c) {
+      a = b = c;
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, c -> c"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ParenExpr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, int* c) {
+      a = (b = c);
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, c -> c"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ParenExpr2) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* a, int* b, int* c) {
+      (a = b) = c;
+      return a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, c -> c"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/expr.cc b/lifetime_analysis/test/expr.cc
new file mode 100644
index 0000000..de0b316
--- /dev/null
+++ b/lifetime_analysis/test/expr.cc
@@ -0,0 +1,40 @@
+// 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 various types of expressions.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, IncrementAndDecrement) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* prefix_inc(int* p) {
+      return ++p;
+    }
+    int* prefix_dec(int* p) {
+      return --p;
+    }
+    int* postfix_inc(int* p) {
+      return p++;
+    }
+    int* postfix_dec(int* p) {
+      return p--;
+    }
+  )"),
+              LifetimesAre({{"prefix_inc", "a -> a"},
+                            {"prefix_dec", "a -> a"},
+                            {"postfix_inc", "a -> a"},
+                            {"postfix_dec", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/function_calls.cc b/lifetime_analysis/test/function_calls.cc
new file mode 100644
index 0000000..93f56a7
--- /dev/null
+++ b/lifetime_analysis/test/function_calls.cc
@@ -0,0 +1,494 @@
+// 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 propagating pointees through function calls.
+//
+// Not every test that contains a function call should go here -- just those
+// that test some specific aspect of the logic that propagates pointees through
+// function calls.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, SimpleFnIdentity) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int* a) {
+      return a;
+    }
+    int* target(int* a) {
+      return f(a);
+    }
+  )"),
+              LifetimesAre({{"f", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleFnStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f() {
+      static int i = 42;
+      return &i;
+    }
+    int* target() {
+      return f();
+    }
+  )"),
+              LifetimesAre({{"f", "-> static"}, {"target", "-> static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleFnStaticOutParam) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int** p) {
+      static int i = 42;
+      *p = &i;
+    }
+    int* target() {
+      int* p;
+      f(&p);
+      return p;
+    }
+  )"),
+              LifetimesAre({{"f", "(static, a)"}, {"target", "-> static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleFnIdentityArg1) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int* a, int* b) {
+      return a;
+    }
+    int* target(int* a, int* b) {
+      return f(b, a);
+    }
+  )"),
+              LifetimesAre({{"f", "a, b -> a"}, {"target", "a, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleFnCall) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+    int* target(int* a, int* b) {
+      return get_lesser_of(a, b);
+    }
+  )"),
+      LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, RefFnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* get_as_ptr(int& a) {
+      return &a;
+    }
+    int* target(int& a) {
+      return get_as_ptr(a);
+    }
+  )"),
+              LifetimesAre({{"get_as_ptr", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_NonPointerParameter) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int id(int i) {
+      return i;
+    }
+    int target() {
+      return id(42);
+    }
+  )"),
+              LifetimesAre({{"id", "()"}, {"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_DoublePointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int** f(int** pp) {
+      return pp;
+    }
+    int** target(int** pp) {
+      return f(pp);
+    }
+  )"),
+              LifetimesAre(
+                  {{"f", "(a, b) -> (a, b)"}, {"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_DoublePointerDeref) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int** pp) {
+      return *pp;
+    }
+    int* target(int** pp) {
+      return f(pp);
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b) -> a"}, {"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int** f(int** pp1, int** pp2) {
+      return pp1;
+    }
+    int* target(int* p1, int* p2) {
+      return *f(&p1, &p2);
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), (c, d) -> (a, b)"},
+                            {"target", "a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithControlFlow) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int** f(int** pp1, int** pp2) {
+      if (**pp1 < **pp2) {
+        *pp1 = *pp2;
+      }
+      return pp1;
+    }
+    int* target(int* p1, int* p2) {
+      return *f(&p1, &p2);
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), (a, c) -> (a, b)"},
+                            {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithOuterConst) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int** pp1, int** const pp2) {
+      *pp1 = *pp2;
+    }
+    int* target1(int* p1, int* p2) {
+      // Making this call can cause p1 to be overwritten with p2...
+      f(&p1, &p2);
+      return p1;
+    }
+    int* target2(int* p1, int* p2) {
+      // ...and it can also cause p2 to be overwritten with p1.
+      //
+      // The `const` only causes `pp2` itself to be const, but `*pp2` and
+      // `**pp2` are both non-const. In other words, from the lifetimes of `f()`
+      // alone, it would be entirely possible for it to do `*pp2 = *pp1`.
+      f(&p1, &p2);
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), (a, c)"},
+                            {"target1", "a, a -> a"},
+                            {"target2", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithMiddleConst) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int** pp1, int* const * pp2) {
+      *pp1 = *pp2;
+    }
+    int* target1(int* p1, int* p2) {
+      // Making this call can cause p1 to be overwritten with p2...
+      f(&p1, &p2);
+      return p1;
+    }
+    int* target2(int* p1, int* p2) {
+      // ...but it can't cause p2 to be overwritten with p1.
+      f(&p1, &p2);
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), (a, c)"},
+                            {"target1", "a, a -> a"},
+                            {"target2", "a, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_MultipleDoublePointerWithInnerConst) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(const int** pp1, int** pp2) {
+      *pp1 = *pp2;
+    }
+    const int* target1(const int* p1, int* p2) {
+      // Making this call can cause p1 to be overwritten with p2.
+      f(&p1, &p2);
+      return p1;
+    }
+    const int* target2(const int* p1, int* p2) {
+      // The analysis concludes that p2 could also be overwritten by p1,
+      // despite the fact that a const int* cannot be converted to an int*.
+      // This is because, when determining what objects the callee might copy,
+      // the analysis looks only at lifetimes in the function signature but not
+      // at whether the objects that these lifetimes refer to can be converted
+      // into one another.
+      // As a result, the lifetimes we infer for target2() are stricter than
+      // they would need to be.
+      f(&p1, &p2);
+      return p2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), (a, c)"},
+                            {"target1", "a, a -> a"},
+                            {"target2", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_TriplePointerWithConst_1) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int*** ppp1, int** const * ppp2) {
+      *ppp1 = *ppp2;
+    }
+    int** target(int* p1, int** pp2) {
+      // - `pp2` cannot be overwritten because of the `const` in the signature
+      //   of `f()`. (Without this, we would infer a local lifetime for the
+      //   return value.)
+      // - `*pp2` can be overwritten.
+      int** pp1 = &p1;
+      f(&pp1, &pp2);
+      return pp2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b, c), (a, b, d)"},
+                            {"target", "a, (a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_TriplePointerWithConst_2) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int*** ppp1, int* const ** ppp2) {
+      **ppp1 = **ppp2;
+    }
+    int* const * target(int* p1, int* const * pp2) {
+      // - `pp2` cannot be overwritten because of the lifetimes in the signature
+      //   of `f()`.
+      // - `*pp2` cannot be overwritten because of the `const` in the signature
+      //   of `f()`.
+      int** pp1 = &p1;
+      f(&pp1, &pp2);
+      return pp2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b, c), (a, d, e)"},
+                            {"target", "a, (b, c) -> (b, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_TriplePointerWithConst_3) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int* const ** ppp1, int* const ** ppp2) {
+      *ppp1 = *ppp2;
+    }
+    int* const * target(int* const p1, int* const * pp2) {
+      // - `pp2` can be overwritten (hence the return value has local lifetime)
+      // - `*pp2` can be overwritten (hence both `p1` and `pp2` have lifetime a)
+      int* const * pp1 = &p1;
+      f(&pp1, &pp2);
+      return pp2;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b, c), (a, b, d)"},
+                            {"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_OutputParam) {
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int* in, int** out) {
+      *out = in;
+    }
+    int* target(int* p) {
+      int* result;
+      f(p, &result);
+      return result;
+    }
+  )"),
+              LifetimesAre({{"f", "a, (a, b)"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_Operator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {};
+    bool operator<(const S& s1, const S& s2) {
+      return false;
+    }
+    bool target(const S& s) {
+      return s < s;
+    }
+  )"),
+              LifetimesAre({{"operator<", "a, b"}, {"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FnCall_PassLambda) {
+  // This test doesn't do anything interesting from a lifetimes point of view.
+  // It's just intended to test that we can instantiate a capture-less lambda
+  // and convert it to a function pointer.
+  EXPECT_THAT(GetLifetimes(R"(
+    void call_callback(void(*callback)()) {
+      // TODO(mboehme): Can't actually call the callback yet because we don't
+      // have support for indirect callees.
+      // callback();
+    }
+
+    void target() {
+      call_callback([] {});
+    }
+  )"),
+              LifetimesContain({{"call_callback", "a"}, {"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleIndirectFnCall) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+    int* target(int* a, int* b) {
+      auto fp = get_lesser_of;
+      return fp(a, b);
+    }
+  )"),
+      LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleIndirectFnCallFwdDecl) {
+  // Tests that the analysis correctly identifies dependencies due to non-call
+  // uses of a function.
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    int* get_lesser_of(int* a, int* b);
+    int* target(int* a, int* b) {
+      auto fp = get_lesser_of;
+      return fp(a, b);
+    }
+    int* get_lesser_of(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+  )"),
+      LifetimesAre({{"get_lesser_of", "a, a -> a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConditionalIndirectFnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* get_first(int* a, int* b) {
+      return a;
+    }
+    int* get_second(int* a, int* b) {
+      return b;
+    }
+    int* target(int* a, int* b) {
+      auto fp = *a < *b ? get_first : get_second;
+      return fp(a, b);
+    }
+  )"),
+              LifetimesAre({{"get_first", "a, b -> a"},
+                            {"get_second", "a, b -> b"},
+                            {"target", "a, a -> a"}}));
+}
+
+// TODO(mboehme): Add a test where we're calling a function with lifetime
+// signature `static -> a`. The analysis should realize that f could return
+// its input pointee. Creating such a test is currently difficult because we
+// don't have lifetime annotations and the inferred lifetime for the return
+// value of f will always be static in this case.
+
+TEST_F(LifetimeAnalysisTest, ComplexFnCallGraph) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+    int* g(int* a, int* b) {
+      return f(a, b);
+    }
+    int* h(int* a, int* b) {
+      return f(a, b);
+    }
+    int* target(int* a, int* b, int* c, int* d) {
+      return f(g(a, b), h(c, d));
+    }
+  )"),
+              LifetimesAre({{"f", "a, a -> a"},
+                            {"g", "a, a -> a"},
+                            {"h", "a, a -> a"},
+                            {"target", "a, a, a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ComplexFnCallGraphUnusedArgs) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int* a, int* b) {
+      if (*a < *b) {
+        return a;
+      }
+      return b;
+    }
+    int* g(int* a, int* b) {
+      return f(a, b);
+    }
+    int* h(int* a, int* b) {
+      return f(a, b);
+    }
+    int* target(int* a, int* b, int* c, int* d) {
+      return f(g(a, b), h(a, b));
+    }
+  )"),
+              LifetimesAre({{"f", "a, a -> a"},
+                            {"g", "a, a -> a"},
+                            {"h", "a, a -> a"},
+                            {"target", "a, a, b, c -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCall) {
+  // Tests that lifetimes of structs are properly propagated (in both
+  // directions) through function calls.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void f(S* s, int* a) {
+      s->a = a;
+    }
+    int* target(S* s, int* a) {
+      f(s, a);
+      return s->a;
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), a"}, {"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructDoubleCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void f(S* s, int* a) {
+      s->a = a;
+    }
+    int* g(S* s) {
+      return s->a;
+    }
+    int* target(S* s, int* a) {
+      f(s, a);
+      return g(s);
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), a"},
+                            {"g", "(a, b) -> a"},
+                            {"target", "(a, b), a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/function_templates.cc b/lifetime_analysis/test/function_templates.cc
new file mode 100644
index 0000000..dd90beb
--- /dev/null
+++ b/lifetime_analysis/test/function_templates.cc
@@ -0,0 +1,154 @@
+// 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 involving function templates.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplatePtr) {
+  EXPECT_THAT(GetLifetimesWithPlaceholder(R"(
+    template <typename T>
+    T* target(T* t) {
+      return t;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplatePtrWithTwoArgs) {
+  EXPECT_THAT(GetLifetimesWithPlaceholder(R"(
+    template <typename T, typename U>
+    T* target(T* t, U* u1, U& u2) {
+      u1 = &u2;
+      return t;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b, c -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplatePtrWithTemplatedStruct) {
+  EXPECT_THAT(GetLifetimesWithPlaceholder(R"(
+    template <typename T>
+    struct S {
+      T t;
+    };
+
+    template <typename T>
+    T* target(S<T*>* s) {
+      return s->t;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplatePtrWithMultipleFunctions) {
+  // The code has both template and non-template functions/code.
+  EXPECT_THAT(GetLifetimesWithPlaceholder(R"(
+    static int x = 3;
+    template <typename T>
+    struct A {
+      T x;
+      T y;
+    };
+    template <typename T>
+    T* target(T* t) {
+      return t;
+    }
+    template <typename U>
+    U* target2(U* u) {
+      return u;
+    }
+    int foo(A<int>* a) {
+      return a->x + a->y + x;
+    }
+  )"),
+              LifetimesAre(
+                  {{"target", "a -> a"}, {"target2", "a -> a"}, {"foo", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplateCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    T* t(T* a, T* b) {
+      if (*a > *b) {
+        return a;
+      }
+      return b;
+    }
+    int* target(int* a, int* b) {
+      return t(a, b);
+    }
+  )"),
+              LifetimesContain({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplateCallIgnoreArg) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    T* t(T* a, T* b) {
+      return a;
+    }
+    int* target(int* a, int* b) {
+      return t(a, b);
+    }
+  )"),
+              LifetimesContain({{"target", "a, b -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplateCallPtrInstantiation) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    T* t(T* a, T* b) {
+      if (*a > *b) {
+        return a;
+      }
+      return b;
+    }
+    int** target(int** a, int** b) {
+      return t(a, b);
+    }
+  )"),
+              LifetimesContain({{"target", "(a, b), (a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplateCallIgnoreArgPtrInstantiation) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    T* t(T* a, T* b) {
+      return a;
+    }
+    int** target(int** a, int** b) {
+      return t(a, b);
+    }
+  )"),
+              LifetimesContain({{"target", "(a, b), (c, d) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionTemplateInsideClassTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      template <typename U>
+      U f(T t, U u) {
+        return u;
+      }
+    };
+    int* target(S<int *>& s, int* p1, int* p2) {
+      return s.f(p1, p2);
+    }
+  )"),
+              LifetimesContain({{"target", "(a, b), c, d -> d"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/inheritance.cc b/lifetime_analysis/test/inheritance.cc
new file mode 100644
index 0000000..cf4a2a4
--- /dev/null
+++ b/lifetime_analysis/test/inheritance.cc
@@ -0,0 +1,281 @@
+// 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 involving inheritance.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritance) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct [[clang::annotate("lifetime_params", "a")]] B {
+  [[clang::annotate("member_lifetimes", "a")]]
+  int* a;
+};
+struct S : public B {
+};
+int* target(S* s, int* a) {
+  s->a = a;
+  return s->a;
+}
+  )"),
+              LifetimesAre({{"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       DISABLED_StructInheritanceCallTrivialDefaultConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct T {};
+    struct S: public T {
+      S(): T() {}
+      int* a;
+    };
+    void target() {
+      S s;
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInheritanceCallBaseConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] T {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+      T(int* b): b(b) {}
+    };
+    struct S: public T {
+      S(int* a, int* b): a(a), T(b) {}
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    int* target(int* a, int* b) {
+      S s(a, b);
+      return s.b;
+    }
+  )"),
+              LifetimesContain({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInheritanceCallBaseConstructorTypedef) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] T {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+      T(int* b): b(b) {}
+    };
+    using U = T;
+    struct S: public U {
+      S(int* a, int* b): a(a), T(b) {}
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    int* target(int* a, int* b) {
+      S s(a, b);
+      return s.b;
+    }
+  )"),
+              LifetimesContain({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       StructInheritanceCallBaseConstructorTypedefBaseInit) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] T {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+      T(int* b): b(b) {}
+    };
+    using U = T;
+    struct S: public T {
+      S(int* a, int* b): a(a), U(b) {}
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    int* target(int* a, int* b) {
+      S s(a, b);
+      return s.b;
+    }
+  )"),
+              LifetimesContain({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritanceWithMethod) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+struct [[clang::annotate("lifetime_params", "a")]] B {
+  [[clang::annotate("member_lifetimes", "a")]]
+  int* a;
+  int* f() { return a; }
+};
+struct S : public B {
+};
+int* target(S* s, int* a) {
+  s->a = a;
+  return s->f();
+}
+  )"),
+      LifetimesAre({{"B::f", "(a, b): -> a"}, {"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritanceWithMethodInDerived) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+struct [[clang::annotate("lifetime_params", "a")]] B {
+  [[clang::annotate("member_lifetimes", "a")]]
+  int* a;
+};
+struct S : public B {
+  int* f() { return a; }
+};
+int* target(S* s, int* a) {
+  s->a = a;
+  return s->f();
+}
+  )"),
+      LifetimesAre({{"S::f", "(a, b): -> a"}, {"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritanceChained) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+struct [[clang::annotate("lifetime_params", "a")]] A {
+  [[clang::annotate("member_lifetimes", "a")]]
+  int* a;
+};
+struct B : public A {
+  int* f() { return a; }
+};
+struct S : public B {
+};
+int* target(S* s, int* a) {
+  s->a = a;
+  return s->f();
+}
+  )"),
+      LifetimesAre({{"B::f", "(a, b): -> a"}, {"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritanceWithSwappedTemplateArgs) {
+  // Base and Derived have template arguments where the order is swapped, so
+  // if the code reuse the same vector representation for the lifetimes
+  // Derived (T, U) for the base class where Base has (U, T) this code fails.
+  EXPECT_THAT(GetLifetimes(R"(
+template <typename U, typename T>
+struct Base {
+  T base_t;
+  U base_u;
+};
+
+template <typename T, typename U>
+struct Derived : public Base<U, T> {
+  T derived_t;
+  U derived_u;
+};
+
+int* target(Derived<int*, float*>* d, int* t1, int* t2) {
+  d->derived_t = t1;
+  d->base_t = t2;
+  return d->derived_t;
+}
+  )"),
+              // The lifetime for Derived::derived_t should also be
+              // Base::base_t. See discussions at cl/411724984.
+              LifetimesAre({{"target", "(<a, b>, c), a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructSimpleInheritanceWithDoubledTemplateArgs) {
+  // Base and Derived have different number of template arguments.
+  // Similar test case as StructSimpleInheritanceWithSwappedTemplateArgs.
+  EXPECT_THAT(GetLifetimes(R"(
+template <typename T, typename U>
+struct Base {
+  T base_t;
+  U base_u;
+};
+
+template <typename T>
+struct Derived : public Base<T, T> {
+  T derived_t;
+};
+
+int* target(Derived<int*>* d, int* t1, int* t2, int* t3) {
+  d->derived_t = t1;
+  d->base_t = t2;
+  d->base_u = t3;
+  return d->derived_t;
+}
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       StructSimpleInheritanceWithTemplateSubstitutedAndArgs) {
+  // Base is a template type and has different number of template arguments from
+  // Derived. Similar test case as
+  // StructSimpleInheritanceWithSwappedTemplateArgs.
+  EXPECT_THAT(GetLifetimes(R"(
+template <typename T>
+struct Base {
+  T base_t;
+};
+
+template <typename B, typename T>
+struct Derived : public B {
+  T derived_t;
+};
+
+int* target(Derived<Base<int*>, int*>* d, int* t1, int* t2) {
+  d->derived_t = t1;
+  d->base_t = t2;
+  return d->derived_t;
+}
+  )"),
+              LifetimesAre({{"target", "(<a, b>, c), b, a -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, PassDerivedByValue) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct [[clang::annotate("lifetime_params", "a")]] B {
+  [[clang::annotate("member_lifetimes", "a")]]
+  int* a;
+  int* f() { return a; }
+};
+struct S : public B {
+};
+int* target(S s) {
+  return s.f();
+}
+  )"),
+              LifetimesAre({{"B::f", "(a, b): -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, PassDerivedByValue_BaseIsTemplate) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+template <class T>
+struct B {
+  T a;
+  T f() { return a; }
+};
+template <class T>
+struct S : public B<T> {
+};
+int* target(S<int *> s) {
+  return s.f();
+}
+  )"),
+      LifetimesAre({{"B<int *>::f", "(a, b): -> a"}, {"target", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/initialization.cc b/lifetime_analysis/test/initialization.cc
new file mode 100644
index 0000000..5998a6a
--- /dev/null
+++ b/lifetime_analysis/test/initialization.cc
@@ -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
+
+// Tests for initialization.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+// TODO(danakj): Crashes trying to find the initializer expression under
+// MaterializeTemporaryExpr. Should be improved by cl/414032764.
+TEST_F(LifetimeAnalysisTest, DISABLED_VarDeclReferenceToRecordTemporary) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    int* target(int* a) {
+      const S<int*>& s = S<int*>{a};
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VarDeclReferenceToRecordTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*>* target(S<int*>* a) {
+      S<int*>& b = *a;
+      return &b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VarDeclReferenceToRecordNoTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    S* target(S* a) {
+      S& b = *a;
+      return &b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorInitReferenceToRecord) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    template <class Ref>
+    struct R {
+      R(S& s): s(s) {}
+      Ref s;
+    };
+    int* target(S* a) {
+      R<S&> r(*a);
+      return r.s.a;
+    }
+  )"),
+              LifetimesAre({{"R<S &>::R", "(a, b, c): (a, b)"},
+                            {"target", "(a, b) -> a"}}));
+}
+
+// TODO(danakj): Fails because a nested TransferMemberExpr() ends up looking for
+// the field from the outer expr on the object of the inner expr.
+//
+// The code:
+// ObjectSet struct_points_to =
+//     points_to_map.GetExprObjectSet(member->getBase());
+//
+// The AST:
+// MemberExpr 0x4027d3f2628 'int *':'int *' lvalue .p 0x4027d3f7338
+// `-MemberExpr 0x4027d3f25f8 'S<int *>':'struct S<int *>' lvalue .s
+//   0x4027d3f74c0
+//   `-DeclRefExpr 0x4027d3f25d8 'R<int *>':'struct R<int *>' lvalue Var
+//     0x4027d3f6cd0 'r' 'R<int *>':'struct R<int *>'
+//
+// The p field is on struct S, but the code tries to find it on an object
+// of type R<int *>.
+TEST_F(LifetimeAnalysisTest, MemberInitReferenceToRecord) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename P>
+    struct S {
+      P p;
+    };
+    template<typename P>
+    struct [[clang::annotate("lifetime_params", "a")]] R {
+      R(P p): ss{p} {}
+      S<P> ss;
+      [[clang::annotate("member_lifetimes", "a")]]
+      S<P>& s{ss};
+    };
+    int* target(int* a) {
+      R<int*> r(a);
+      return r.s.p;
+    }
+  )"),
+      LifetimesAre({{"R<int *>::R", "(<a> [b], b): a"}, {"target", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/initializers.cc b/lifetime_analysis/test/initializers.cc
new file mode 100644
index 0000000..5eafe80
--- /dev/null
+++ b/lifetime_analysis/test/initializers.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 involving initializers.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest,
+       ReturnStructFieldFromMultipleInitializersConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T i) : i(i) {}
+      T i;
+    };
+    int* ConstructorSyntax(int* a, int* b, bool cond) {
+      return (cond ? S<int*>{a} : S<int*>{b}).i;
+    }
+    int* CastSyntax(int* a, int* b, bool cond) {
+      return (cond ? S<int*>(a) : S<int*>(b)).i;
+    }
+  )"),
+              LifetimesAre({
+                  {"S<int *>::S", "(a, b): a"},
+                  {"ConstructorSyntax", "a, a, () -> a"},
+                  {"CastSyntax", "a, a, () -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ReturnStructFieldFromMultipleInitializersInitList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T i;
+    };
+    int* target(int* a, int* b, bool cond) {
+      return (cond ? S<int*>{a} : S<int*>{b}).i;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ReturnStructFromMultipleInitializersConstructSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T i) : i(i) {}
+      T i;
+    };
+    S<int*> target(int* a, int* b) {
+      return true ? S<int*>{a} : S<int*>{b};
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructFromMultipleInitializersCastSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T i) : i(i) {}
+      T i;
+    };
+    S<int*> target(int* a, int* b) {
+      return true ? S<int*>(a) : S<int*>(b);
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ReturnStructFromMultipleInitializersInitListSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T i;
+    };
+    S<int*> target(int* a, int* b) {
+      return true ? S<int*>{a} : S<int*>{b};
+    }
+  )"),
+              LifetimesAre({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithMultipleInitializersConstructorSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T i) : i(i) {}
+      T i;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s = true ? S{a} : S{b};
+      return s.i;
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithMultipleInitializersCastSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T i) : i(i) {}
+      T i;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s = true ? S(a) : S(b);
+      return s.i;
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithMultipleInitializersInitListSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T i;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s = true ? S<int*>{a} : S<int*>{b};
+      return s.i;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ConstructorInitWithMultipleInitializersConstructorSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : r(true ? R{a} : R{b}) {}
+      R<T> r;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+              LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                            {"S<int *>::S", "(a, b): a, a"},
+                            {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ConstructorInitWithMultipleInitializersCastSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : r(true ? R(a) : R(b)) {}
+      R<T> r;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+              LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                            {"S<int *>::S", "(a, b): a, a"},
+                            {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ConstructorInitWithMultipleInitializersInitListSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : r(true ? R<T>{a} : R<T>{b}) {}
+      R<T> r;
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a, a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       MemberInitWithMultipleInitializersConstructorSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : a(a), b(b) {}
+      T a;
+      T b;
+      R<T> r{true ? R{a} : R{b}};
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+              LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                            {"S<int *>::S", "(a, b): a, a"},
+                            {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MemberInitWithMultipleInitializersCastSyntax) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : a(a), b(b) {}
+      T a;
+      T b;
+      R<T> r{true ? R(a) : R(b)};
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+              LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                            {"S<int *>::S", "(a, b): a, a"},
+                            {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorInitWithMultiplePointers) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T, typename U, typename V>
+    struct S {
+      S(T a, U b) : r(true ? a : b) {}
+      R<V> r;
+    };
+    int* target(int* a, int* b) {
+      S<int*, int*, int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+      LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                    {"S<int *, int *, int *>::S", "(<b, c, a>, d): a, a"},
+                    {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ConstructorInitWithMultiplePointersAndStoresFields) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T, typename U, typename V>
+    struct S {
+      S(T a, U b) : a_(a), b_(b), r(true ? a : b) {}
+      T a_;
+      U b_;
+      R<V> r;
+    };
+    int* target(int* a, int* b) {
+      S<int*, int*, int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+      LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                    {"S<int *, int *, int *>::S", "(<a, a, a>, b): a, a"},
+                    {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MemberInitWithMultiplePointers) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      R(T i) : i(i) {}
+      T i;
+    };
+    template <typename T, typename U, typename V>
+    struct S {
+      S(T a, U b) : a(a), b(b) {}
+      T a;
+      U b;
+      R<V> r{true ? a : b};
+    };
+    int* target(int* a, int* b) {
+      S<int*, int*, int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+      LifetimesAre({{"R<int *>::R", "(a, b): a"},
+                    {"S<int *, int *, int *>::S", "(<a, a, a>, b): a, a"},
+                    {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MemberInitWithMultipleInitializersInitListSyntax) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      T i;
+    };
+    template <typename T>
+    struct S {
+      S(T a, T b) : a(a), b(b) {}
+      T a;
+      T b;
+      R<T> r{true ? R<T>{a} : R<T>{b}};
+    };
+    int* target(int* a, int* b) {
+      S<int*> s(a, b);
+      return s.r.i;
+    }
+  )"),
+      LifetimesAre({{"S<int *>::S", "(a, b): a, a"}, {"target", "a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DeclStructInitializerWithConversionOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      T a;
+    };
+    template <typename T>
+    struct S {
+      T a;
+      operator R<T>() { return {a}; }
+    };
+    int* target(int* a) {
+      R<int*> r = S<int*>{a};
+      return r.a;
+    }
+  )"),
+              LifetimesAre({{"S<int *>::operator R", "(a, b): -> a"},
+                            {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DeclStructInitializerFromCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      T a;
+    };
+    template <typename T>
+    struct R<T> f(T a) {
+      return R<T>{a};
+    }
+    int* target(int* a) {
+      R<int*> r = f<int*>(a);
+      return r.a;
+    }
+  )"),
+              LifetimesAre({{"f", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructInitializerWithConversionOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct R {
+      T a;
+    };
+    template <typename T>
+    struct S {
+      T a;
+      operator R<T>() { return {a}; }
+    };
+    R<int*> target(int* a) {
+      return S<int*>{a};
+    }
+  )"),
+              LifetimesAre({{"S<int *>::operator R", "(a, b): -> a"},
+                            {"target", "a -> a"}}));
+}
+
+// TODO(danakj): Crashes due to operator() not being a CXXConstructExpr, but
+// SetExprObjectSetRespectingType only handles CXXConstructExpr for record
+// types.
+TEST_F(LifetimeAnalysisTest,
+       DISABLED_ConstructorInitializerWithConversionOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    template <typename T>
+    struct R {
+      T a;
+      operator S<T>() { return {a}; }
+    };
+
+    // This initializes the `s` field from a constructor initializer.
+    template <typename T>
+    struct QConstructor {
+      QQConstructor(T a) : s(R<T>{a}) {}
+      S<T> s;
+    };
+    int* constructor(int* a) {
+      return QQConstructor<int*>{a}.s.a;
+    }
+
+    // This initializes the `s` field from a transparent InitListExpr on a
+    // member initializer.
+    template <typename T>
+    struct QMember {
+      QMember(T a) : a(a) {}
+      T a;
+      S<T> s{S<T>(R<T>{a})};
+    };
+    int* member(int* a) {
+      return QMember<int*>{a}.s.a;
+    }
+)"),
+              LifetimesAre({{"QConstructor<int *>::QConstructor", "(a, b): a"},
+                            {"QMember<int *>::QMember", "(a, b): a"},
+                            {"constructor", "a -> a"},
+                            {"member", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInitializerWithCtorCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T a) : a(a) {}
+      T a;
+    };
+    int* TransparentInitListExpr(int* a) {
+      S<int*> s{S<int*>(a)};
+      return s.a;
+    }
+    int* CastSyntax(int* a) {
+      S<int*> s((S<int*>(a)));
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"S<int *>::S", "(a, b): a"},
+                            {"TransparentInitListExpr", "a -> a"},
+                            {"CastSyntax", "a -> a"}}));
+}
+
+// TODO(danakj): Crashes because the initializer expression is a
+// CXXStaticCastExpr, and operator() is not a CXXConstructExpr, but
+// SetExprObjectSetRespectingType only handles CXXConstructExpr for record
+// types.
+TEST_F(LifetimeAnalysisTest, DISABLED_StaticCastInitializer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    template <typename T>
+    struct R {
+      T a;
+      operator S<T>() { return {a}; }
+    };
+    int* target(int* a) {
+      return static_cast<S<int*>>(R<int*>{a}).a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/lifetime_analysis_test.cc b/lifetime_analysis/test/lifetime_analysis_test.cc
new file mode 100644
index 0000000..b608eb6
--- /dev/null
+++ b/lifetime_analysis/test/lifetime_analysis_test.cc
@@ -0,0 +1,177 @@
+// 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 "lifetime_analysis/test/lifetime_analysis_test.h"
+
+#include <fstream>
+#include <ostream>
+#include <string>
+#include <utility>
+#include <variant>
+
+#include "lifetime_annotations/test/run_on_code.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+void SaveDotFile(absl::string_view dot, absl::string_view filename_base,
+                 absl::string_view test_name, absl::string_view description) {
+  std::string base_path =
+      absl::StrCat(testing::TempDir(), "/", test_name, ".", filename_base);
+  std::ofstream out(absl::StrCat(base_path, ".dot"));
+  if (!out) {
+    llvm::errs() << "Error opening dot file: " << strerror(errno) << "\n";
+    return;
+  }
+  out << dot;
+  if (!out) {
+    llvm::errs() << "Error writing dot file: " << strerror(errno) << "\n";
+    return;
+  }
+  out.close();
+  if (system(
+          absl::StrCat("dot ", base_path, ".dot -T svg -o ", base_path, ".svg")
+              .c_str()) != 0) {
+    llvm::errs() << "Error invoking graphviz. dot file can be found at: "
+                 << base_path << ".dot\n";
+    return;
+  }
+}
+
+}  // namespace
+
+void LifetimeAnalysisTest::TearDown() {
+  if (HasFailure()) {
+    for (const auto& [func, debug_info] : debug_info_map_) {
+      std::cerr << debug_info.ast << "\n";
+
+      std::cerr << debug_info.object_repository << "\n";
+
+      const char* test_name =
+          testing::UnitTest::GetInstance()->current_test_info()->name();
+
+      SaveDotFile(debug_info.points_to_map_dot,
+                  absl::StrCat(func, "_points_to"), test_name,
+                  "Points-to map of exit block");
+      SaveDotFile(debug_info.cfg_dot, absl::StrCat(func, "_cfg"), test_name,
+                  "Control-flow graph");
+    }
+    std::cerr << "Debug graphs can be found in " << testing::TempDir()
+              << std::endl;
+  }
+}
+
+std::string LifetimeAnalysisTest::QualifiedName(
+    const clang::FunctionDecl* func) {
+  // TODO(veluca): figure out how to name overloaded functions.
+  std::string str;
+  llvm::raw_string_ostream ostream(str);
+  func->printQualifiedName(ostream);
+  ostream.flush();
+  return str;
+}
+
+NamedFuncLifetimes LifetimeAnalysisTest::GetLifetimes(
+    llvm::StringRef source_code, const GetLifetimesOptions& options) {
+  NamedFuncLifetimes tu_lifetimes;
+
+  auto test = [&tu_lifetimes, &options, this](
+                  clang::ASTContext& ast_context,
+                  const LifetimeAnnotationContext& lifetime_context) {
+    // This will get called even if the code contains compilation errors.
+    // So we need to check to avoid performing an analysis on code that
+    // doesn't compile.
+    if (ast_context.getDiagnostics().hasUncompilableErrorOccurred() &&
+        !analyze_broken_code_) {
+      tu_lifetimes.Add("", "Compilation error -- see log for details");
+      return;
+    }
+
+    auto result_callback = [&tu_lifetimes, &options](
+                               const clang::FunctionDecl* func,
+                               const FunctionLifetimesOrError&
+                                   lifetimes_or_error) {
+      if (std::holds_alternative<FunctionAnalysisError>(lifetimes_or_error)) {
+        tu_lifetimes.Add(
+            QualifiedName(func),
+            absl::StrCat(
+                "ERROR: ",
+                std::get<FunctionAnalysisError>(lifetimes_or_error).message));
+        return;
+      }
+      const auto& func_lifetimes =
+          std::get<FunctionLifetimes>(lifetimes_or_error);
+
+      // Do not insert in the result set implicitly-defined constructors or
+      // assignment operators.
+      if (auto* constructor =
+              clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+        if (constructor->isImplicit() && !options.include_implicit_methods) {
+          return;
+        }
+      }
+      if (auto* method = clang::dyn_cast<clang::CXXMethodDecl>(func)) {
+        if (method->isImplicit() && !options.include_implicit_methods) {
+          return;
+        }
+      }
+
+      tu_lifetimes.Add(QualifiedName(func), NameLifetimes(func_lifetimes));
+    };
+
+    FunctionDebugInfoMap func_ptr_debug_info_map;
+    llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+        analysis_result;
+    if (options.with_template_placeholder) {
+      AnalyzeTranslationUnitWithTemplatePlaceholder(
+          ast_context.getTranslationUnitDecl(), lifetime_context,
+          result_callback,
+          /*diag_reporter=*/{}, &func_ptr_debug_info_map);
+    } else {
+      analysis_result = AnalyzeTranslationUnit(
+          ast_context.getTranslationUnitDecl(), lifetime_context,
+          /*diag_reporter=*/{}, &func_ptr_debug_info_map);
+
+      for (const auto& [func, lifetimes_or_error] : analysis_result) {
+        result_callback(func, lifetimes_or_error);
+      }
+    }
+
+    for (auto& [func, debug_info] : func_ptr_debug_info_map) {
+      debug_info_map_.try_emplace(func->getDeclName().getAsString(),
+                                  std::move(debug_info));
+    }
+  };
+
+  if (!runOnCodeWithLifetimeHandlers(source_code, test,
+                                     {"-fsyntax-only", "-std=c++17"})) {
+    // We need to disambiguate between two cases:
+    // - We were unable to run the analysis at all (because there was some
+    //   internal error)
+    //   In this case, `tu_lifetimes` will be empty, so add a corresponding
+    //   note here.
+    // - The analysis emitted an error diagnostic, which will also cause us to
+    //   end up here.
+    //   In this case, `tu_lifetimes` already contains an error empty, so we
+    //   don't need to do anything.
+    if (tu_lifetimes.Entries().empty()) {
+      tu_lifetimes.Add("", "Error running dataflow analysis");
+    }
+  }
+
+  return tu_lifetimes;
+}
+
+NamedFuncLifetimes LifetimeAnalysisTest::GetLifetimesWithPlaceholder(
+    llvm::StringRef source_code) {
+  GetLifetimesOptions options;
+  options.with_template_placeholder = true;
+  return GetLifetimes(source_code, options);
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/lifetime_analysis_test.h b/lifetime_analysis/test/lifetime_analysis_test.h
new file mode 100644
index 0000000..fea24d5
--- /dev/null
+++ b/lifetime_analysis/test/lifetime_analysis_test.h
@@ -0,0 +1,49 @@
+// 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 DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_TEST_LIFETIME_ANALYSIS_TEST_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_TEST_LIFETIME_ANALYSIS_TEST_H_
+
+#include <string>
+
+#include "gtest/gtest.h"
+#include "absl/container/flat_hash_map.h"
+#include "lifetime_analysis/analyze.h"
+#include "lifetime_annotations/test/named_func_lifetimes.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+class LifetimeAnalysisTest : public testing::Test {
+ protected:
+  void TearDown() override;
+
+  static std::string QualifiedName(const clang::FunctionDecl* func);
+
+  struct GetLifetimesOptions {
+    GetLifetimesOptions()
+        : with_template_placeholder(false), include_implicit_methods(false) {}
+    bool with_template_placeholder;
+    bool include_implicit_methods;
+  };
+
+  NamedFuncLifetimes GetLifetimes(
+      llvm::StringRef source_code,
+      const GetLifetimesOptions& options = GetLifetimesOptions());
+
+  NamedFuncLifetimes GetLifetimesWithPlaceholder(llvm::StringRef source_code);
+
+  void AnalyzeBrokenCode() { analyze_broken_code_ = true; }
+
+ private:
+  absl::flat_hash_map<std::string, FunctionDebugInfo> debug_info_map_;
+  bool analyze_broken_code_ = false;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_TEST_LIFETIME_ANALYSIS_TEST_H_
diff --git a/lifetime_analysis/test/lifetime_params.cc b/lifetime_analysis/test/lifetime_params.cc
new file mode 100644
index 0000000..39288c3
--- /dev/null
+++ b/lifetime_analysis/test/lifetime_params.cc
@@ -0,0 +1,87 @@
+// 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 involving lifetime parameters.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, SimpleLifetimeParams) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* x;
+    };
+
+    S target(S s) {
+      return s;
+    }
+  )"),
+              LifetimesContain({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, LifetimeParamsMultiplePointers) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a", "b")]] S {
+      [[clang::annotate("member_lifetimes", "a", "b")]]
+      int** x;
+    };
+
+    S target(S s) {
+      return s;
+    }
+  )"),
+              LifetimesContain({{"target", "([a, b]) -> ([a, b])"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, LifetimeParamsMultiplePointersMultipleMembers) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a", "b")]] S {
+      [[clang::annotate("member_lifetimes", "a", "b")]]
+      int** x;
+      [[clang::annotate("member_lifetimes", "b", "a")]]
+      int** y;
+    };
+
+    int** ret_x(S s) {
+      return s.x;
+    }
+
+    int** ret_y(S s) {
+      return s.y;
+    }
+  )"),
+              LifetimesAre({{"ret_y", "([a, b]) -> (b, a)"},
+                            {"ret_x", "([a, b]) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, LifetimeParamsNested) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a", "b")]] T {
+      [[clang::annotate("member_lifetimes", "a", "b")]]
+      int** x;
+    };
+
+    struct [[clang::annotate("lifetime_params", "a", "b")]] S {
+      [[clang::annotate("member_lifetimes", "b", "a")]]
+      T t;
+    };
+
+    int** target(S s) {
+      return s.t.x;
+    }
+  )"),
+              LifetimesContain({{"target", "([a, b]) -> (b, a)"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/records.cc b/lifetime_analysis/test/records.cc
new file mode 100644
index 0000000..2533e15
--- /dev/null
+++ b/lifetime_analysis/test/records.cc
@@ -0,0 +1,1179 @@
+// 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 involving (non-template) records (structs, classes, unions).
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, MembersWithSameAnnotationMergeLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* i;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* j;
+    };
+    void target(S* s, int* p, int* q) {
+      s->i = p;
+      s->j = q;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructsWithTemplateFieldsDoesNotMergeLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename A, typename B>
+    struct S { A i; B j; };
+    void target(S<int*, int*>* s, int* p, int* q) {
+      s->i = p;
+      s->j = q;
+    }
+  )"),
+              LifetimesAre({{"target", "(<a, b>, c), a, b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithArrayMergesLifetimes) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename A>
+    struct S { A array; };
+    void target(S<int**>* s, int* p, int* q) {
+      s->array[0] = p;
+      s->array[1] = q;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b, c), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, DeclRecordWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename P>
+    struct S { P p; };
+    int* target(int* a, int* b, bool cond) {
+      S<int*> s = cond ? S<int*>{a} : S<int*>{b};
+      return s.p;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnRecordWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename P>
+    struct S { P p; };
+    S<int*> target(int* a, int* b, bool cond) {
+      return cond ? S<int*>{a} : S<int*>{b};
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MaterializeRecordWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename P>
+    struct S { P p; };
+    int* target(int* a, int* b, bool cond) {
+      return (cond ? S<int*>{a} : S<int*>{b}).p;
+    }
+  )"),
+              LifetimesAre({{"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorInitRecordWithConditionalOperator) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename P>
+    struct S { P p; };
+    template <typename P>
+    struct T {
+      T(int* a, int* b, bool cond) : s(cond ? S<int*>{a} : S<int*>{b}) {}
+      S<P> s;
+    };
+    int* target(int* a, int* b, bool cond) {
+      T<int*> t(a, b, cond);
+      return t.s.p;
+    }
+  )"),
+              LifetimesAre({{"T<int *>::T", "(a, b): a, a, ()"},
+                            {"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MemberInitRecordWithConditionalOperator) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename P>
+    struct S { P p; };
+    template <typename A, typename B, typename P>
+    struct T {
+      T(int* a, int* b, bool cond) : a(a), b(b), cond(cond) {}
+      A a;
+      B b;
+      bool cond;
+      S<P> s{cond ? S<int*>{a} : S<int*>{b}};
+    };
+    int* target(int* a, int* b, bool cond) {
+      T<int*, int*, int*> t(a, b, cond);
+      return t.s.p;
+    }
+  )"),
+      LifetimesAre({{"T<int *, int *, int *>::T", "(<a, a, a>, b): a, a, ()"},
+                    {"target", "a, a, () -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleStruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+    };
+    void target(S* s, int* a, int* b) {
+      s->a = a;
+      s->b = b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleUnion) {
+  EXPECT_THAT(GetLifetimes(R"(
+    union [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+    };
+    void target(S* s, int* a, int* b) {
+      s->a = a;
+      s->b = b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructReference) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+    };
+    void target(S& s, int* a, int* b) {
+      s.a = a;
+      s.b = b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), a, a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructValue) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    int* target(S s) {
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMultiplePtrsSameLifetime) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a", "a")]]
+      int*** a;
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** b;
+    };
+    void f(S& s) {
+      **s.a = *s.b;
+    }
+    void target(int** a, int* b) {
+       S s{&a, &b};
+       f(s);
+    }
+  )"),
+              LifetimesContain({{"target", "(a, b), a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructNonLocalPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    int* target(S* s) {
+      return s->a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberStructInitializedWithInitializerList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] T {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S(int* a): t{a} { }
+      [[clang::annotate("member_lifetimes", "a")]]
+      T t;
+    };
+    int* target(int* a) {
+      return S{a}.t.a;
+    }
+  )"),
+              LifetimesAre({{"S::S", "(a, b): a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int a;
+    };
+    int* target(S* s) {
+      return &s->a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructReferenceMember) {
+  // This is a regression test for a bug where we were not treating accesses to
+  // member variables of reference type correctly.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S1 {
+      int a;
+    };
+    struct [[clang::annotate("lifetime_params", "a")]] S2 {
+      [[clang::annotate("member_lifetimes", "a")]]
+      S1 &s1;
+    };
+    int& target(S2* s2) {
+      // Make sure we can find the field S1::a. This is to ensure that our
+      // member access for s2->s1 is in fact returning an object of type S1
+      // (not S1&).
+      s2->s1.a = 5;
+      return s2->s1.a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructStaticMemberFunction) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      static int* f(int* x) { return x; }
+    };
+    int* target(int* a) {
+      return S::f(a);
+    }
+  )"),
+              LifetimesAre({{"S::f", "a -> a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFunction) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      int* f() { return a; }
+    };
+  )"),
+              LifetimesAre({{"S::f", "(a, b): -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFunctionExplicitThis) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      int* f() { return this->a; }
+    };
+  )"),
+              LifetimesAre({{"S::f", "(a, b): -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFunctionCall) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      int* f() { return a; }
+    };
+    int* target(S* s) {
+      return s->f();
+    }
+  )"),
+      LifetimesAre({{"S::f", "(a, b): -> a"}, {"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFunctionCallDot) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      int* f() { return a; }
+    };
+    int* target(S* s) {
+      return (*s).f();
+    }
+  )"),
+      LifetimesAre({{"S::f", "(a, b): -> a"}, {"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFunctionComplexCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      void set(int* x) { a = x; }
+      int* f() { return a; }
+    };
+    int* target(S* s, int* b) {
+      s->set(b);
+      return (*s).f();
+    }
+  )"),
+              LifetimesAre({{"S::set", "(a, b): a"},
+                            {"S::f", "(a, b): -> a"},
+                            {"target", "(a, b), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructReturnAddressOfMemberFunction) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      static void f();
+    };
+    typedef void (*funtype)();
+    funtype target() {
+      S s;
+      return s.f;
+    }
+  )"),
+              LifetimesContain({{"target", "-> static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructDefaultConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target() {
+      S s;
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructDefaultConstructor_ExplicitCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target() {
+      S();
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S(int* a) { this->a = a; }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(int* a) {
+      static S s{a};
+    }
+  )"),
+              LifetimesAre({{"S::S", "(a, b): a"}, {"target", "static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCopyConstructorStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(S* x) {
+      static S s = *x;
+    }
+  )"),
+              LifetimesAre({{"target", "(static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorOutputsFieldPointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S(int** field_out) {
+        *field_out = &i;
+      }
+      int i;
+    };
+    int* target() {
+     int* i_out;
+     S s(&i_out);
+     return i_out;
+    }
+  )"),
+              LifetimesAre({{"S::S", "a: (a, b)"},
+                            {"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorOutputsThisPointer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S(S** this_out) {
+        *this_out = this;
+      }
+    };
+    S* target() {
+     S* s_out;
+     S s(&s_out);
+     return s_out;
+    }
+  )"),
+              LifetimesAre({{"S::S", "a: (a, b)"},
+                            {"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       StructConstructorOutputsFieldPointerConstructorInitializer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S(int** field_out) {
+        *field_out = &i;
+      }
+      int i;
+    };
+    struct T {
+      T(int** int_out): s(int_out) {}
+      S s;
+    };
+  )"),
+              LifetimesAre({{"S::S", "a: (a, b)"}, {"T::T", "a: (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       StructConstructorOutputsThisPointerConstructorInitializer) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S(S** this_out) {
+        *this_out = this;
+      } 
+    };
+    struct T {
+      T(S** this_out): s(this_out) {}
+      S s;
+    };
+  )"),
+              LifetimesAre({{"S::S", "a: (a, b)"}, {"T::T", "a: (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorOutputsThisPointerInitMember) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S(S** this_out) {
+        *this_out = this;
+      }
+    };
+    static S* static_s_ptr;
+    struct T {
+      T() {}
+      S s{&static_s_ptr};
+    };
+  )"),
+              LifetimesAre({{"S::S", "a: (a, b)"}, {"T::T", "static:"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorInitializers) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S(int* a): a(a) { }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a = nullptr;
+      // The following members don't affect lifetimes, but we keep them
+      // around to make sure that the related code is exercised.
+      int b = 0;
+      // This member points into the struct itself, forcing the lifetime
+      // parameter in the constructor to be the same as the lifetime of the
+      // object itself.
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* c = &b;
+      int d;
+    };
+  )"),
+              LifetimesAre({{"S::S", "(a, a): a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorStaticPtr) {
+  // TODO(veluca): this is overly restrictive in the same way as
+  // StaticPointerOutParam.
+  EXPECT_THAT(GetLifetimes(R"(
+    static int x;
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S() { a = &x; }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a = nullptr;
+    };
+  )"),
+              LifetimesAre({{"S::S", "(static, a):"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorStaticPtrInitializer) {
+  // TODO(veluca): this is overly restrictive in the same way as
+  // StaticPointerOutParam.
+  EXPECT_THAT(GetLifetimes(R"(
+    static int x;
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S(): a(&x) { }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a = nullptr;
+    };
+  )"),
+              LifetimesAre({{"S::S", "(static, a):"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructConstructorStaticPtrMemberInitializer) {
+  // TODO(veluca): this is overly restrictive in the same way as
+  // StaticPointerOutParam.
+  EXPECT_THAT(GetLifetimes(R"(
+    static int x;
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S() { }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a = &x;
+    };
+  )"),
+              LifetimesAre({{"S::S", "(static, a):"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructMemberFreeFunction) {
+  // Check that calling a method behaves in the same way as a free function.
+  EXPECT_THAT(GetLifetimes(R"(
+    static int x;
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      void f() { a = &x; }
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void f(S& a) {
+      a.a = &x;
+    }
+  )"),
+              LifetimesAre({{"S::f", "(static, a):"}, {"f", "(static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnFieldFromTemporaryStructConstructor) {
+  // S(i) with a single argument produces a clang::CXXFunctionalCastExpr around
+  // a clang::CXXConstructExpr.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      S(int* i) : f(i) {}
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* f;
+    };
+    int* ConstructorSyntax(int* i) {
+      return S{i}.f;
+    }
+    int* CastSyntax(int* i) {
+      return S(i).f;
+    }
+  )"),
+              LifetimesAre({{"S::S", "(a, b): a"},
+                            {"ConstructorSyntax", "a -> a"},
+                            {"CastSyntax", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ReturnFieldFromTemporaryStructConstructorInitList) {
+  // S has no constructors so S{i} produces a clang::InitListExpr.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* f;
+    };
+    int* target(int* i) {
+      return S{i}.f;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnFieldFromTemporaryUnion) {
+  // S has no constructors so S{i} produces a clang::InitListExpr.
+  EXPECT_THAT(GetLifetimes(R"(
+    union [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* f;
+    };
+    int* target(int* i) {
+      return S{i}.f;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInitList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(int* a) {
+      S s{a};
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, UnionInitList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    union [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(int* a) {
+      S s{a};
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCopy) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(S* a, S* b) {
+      *a = *b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), (a, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCopyStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(S* x) {
+      static S s;
+      s = *x;
+    }
+  )"),
+              LifetimesAre({{"target", "(static, a)"}}));
+}
+
+// We fail to initialize the temporary object in a CXXOperatorCallExpr argument,
+// which causes us to assert when we visit the MaterializeTemporaryExpr later.
+TEST_F(LifetimeAnalysisTest, DISABLED_CallExprWithRecordInitializedArguments) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* i;
+    };
+    S callee(const S& s1, const S& s2) {
+      return S{s2.i};
+    }
+    S target(int* a, int* b) {
+      return callee(S{a}, S{b});
+    }
+  )"),
+              LifetimesAre({{"target", "a, b -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructAssignMemberStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    void target(int* x) {
+      static S s;
+      s.a = x;
+    }
+  )"),
+              LifetimesAre({{"target", "static"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCopyExplicit) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      S& operator=(const S& other) {
+        a = other.a;
+        return *this;
+      }
+    };
+    void target(S* a, S* b) {
+      *a = *b;
+    }
+  )"),
+              LifetimesAre({{"S::operator=", "(a, c): (a, b) -> (a, c)"},
+                            {"target", "(a, b), (a, c)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructCopyExplicitNoop) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      S& operator=(const S& other) {
+        return *this;
+      }
+    };
+    void target(S* a, S* b) {
+      *a = *b;
+    }
+  )"),
+              LifetimesAre({{"S::operator=", "(c, d): (a, b) -> (c, d)"},
+                            {"target", "(a, b), (c, d)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructWithStruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] T {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      T t;
+    };
+    int* target(S* s) {
+      return s->t.a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, NonReferenceLikeStruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int a;
+    };
+    void target(S* a, S* b) {
+      a->a = b->a;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructNonReferenceLikeField) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      int a;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* b;
+    };
+    void target(S* a, S* b) {
+      a->a = b->a;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), (c, d)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructAssignToReference) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      int a;
+      [[clang::annotate("member_lifetimes", "a")]]
+      int& b;
+    };
+    void target(S* a, S* b) {
+      a->a = b->a;
+      a->b = b->b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b), (c, d)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, NonReferenceLikeStructCopyAssignment) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int a;
+    };
+    void target(S* a, S* b) {
+      *a = *b;
+    }
+  )"),
+              LifetimesAre({{"target", "a, b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnReferenceLikeStruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* p;
+    };
+    S target() {
+      int i = 42;
+      S s = { &i };
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnNonReferenceLikeStruct) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int a;
+    };
+    S target() {
+      int i = 42;
+      S s = { i };
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnNonReferenceLikeStructCopy) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int a;
+    };
+    S target(S& s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnNonReferenceLikeStructFromTemporary) {
+  // This is a repro for a crash observed on b/228325046.
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {};
+    S target() {
+      return S();
+    }
+  )"),
+              LifetimesAre({{"target", ""}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInnerDoublePtrInitList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** x;
+    };
+
+    void f(int** b) {
+      S s{b};
+      int i = 0;
+      *s.x = &i;
+    }
+  )"),
+              LifetimesAre({{"f",
+                             "ERROR: function returns reference to a local "
+                             "through parameter 'b'"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInnerDoublePtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** x;
+    };
+
+    void g(S* s, int* a) {
+      *s->x = a;
+    }
+
+    void f(int* a, int** b) {
+      S s{b};
+      g(&s, a);
+    }
+  )"),
+              LifetimesAre({{"f", "a, (a, b)"}, {"g", "(a, b), a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInnerDoublePtrAssign) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** x;
+    };
+
+    void g(S* s, int* a) {
+      *s->x = a;
+    }
+
+    int* f(int* a, int** b) {
+      S s{b};
+      g(&s, a);
+      return *b;
+    }
+  )"),
+              LifetimesAre({{"f", "a, (a, b) -> a"}, {"g", "(a, b), a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructInnerDoublePtrParam) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** x;
+    };
+
+    void g(S* s, int* a) {
+      *s->x = a;
+    }
+
+    void f(S& s, int* a, int** b) {
+      s.x = b;
+      g(&s, a);
+    }
+  )"),
+              LifetimesAre({{"f", "(a, b), a, (a, a)"}, {"g", "(a, b), a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, List) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] List {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      List* next;
+      void Append(List& oth) {
+        next = &oth;
+      }
+      int* Get() const {
+        return a;
+      }
+    };
+    int* target(List* l, int* a) {
+      if (l->next) {
+        l->next->a = a;
+      }
+      return l->Get();
+    }
+  )"),
+              LifetimesAre({{"List::Append", "(a, b): (a, a)"},
+                            {"List::Get", "(a, b): -> a"},
+                            {"target", "(a, b), a -> a"}}));
+}
+
+// TODO(danakj): Crashes trying to find the initializer expression under
+// MaterializeTemporaryExpr. Should be improved by cl/414032764.
+TEST_F(LifetimeAnalysisTest, DISABLED_VarDeclReferenceToRecordTemporary) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    int* target(int* a) {
+      const S<int*>& s = S<int*>{a};
+      return s.a;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VarDeclReferenceToRecordTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*>* target(S<int*>* a) {
+      S<int*>& b = *a;
+      return &b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, VarDeclReferenceToRecordNoTemplate) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    S* target(S* a) {
+      S& b = *a;
+      return &b;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> (a, b)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorInitReferenceToRecord) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* a;
+    };
+    template <class Ref>
+    struct R {
+      R(S& s): s(s) {}
+      Ref s;
+    };
+    int* target(S* a) {
+      R<S&> r(*a);
+      return r.s.a;
+    }
+  )"),
+              LifetimesAre({{"R<S &>::R", "(a, b, c): (a, b)"},
+                            {"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MemberInitReferenceToRecord) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    template <typename P>
+    struct S {
+      P p;
+    };
+    template<typename P>
+    struct [[clang::annotate("lifetime_params", "a")]] R {
+      R(P p): ss{p} {}
+      S<P> ss;
+      [[clang::annotate("member_lifetimes", "a")]]
+      S<P>& s{ss};
+    };
+    int* target(int* a) {
+      R<int*> r(a);
+      return r.s.p;
+    }
+  )"),
+      LifetimesAre({{"R<int *>::R", "(<a> [b], b): a"}, {"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturn) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(S<int*>& s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "(a, b) -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnXvalue) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(S<int*> s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> take_by_ref(S<int*>& s) {
+      return s;
+    }
+    S<int*> take_by_value(S<int*> s) {
+      return s;
+    }
+  )"),
+              LifetimesAre({{"take_by_ref", "(a, b) -> a"},
+                            {"take_by_value", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StructTemplateReturnLocal) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> target(int* a) {
+      int i = 42;
+      return { &i };
+    }
+  )"),
+              LifetimesAre({{"target",
+                             "ERROR: function returns reference to a local"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructTemporaryConstructor) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      S(T a) : a(a) {}
+      T a;
+    };
+    S<int*> ConstructorCastSyntax(int* a) {
+      return S(a);
+    }
+    S<int*> ConstructTemporarySyntax(int* a) {
+      return S{a};
+    }
+  )"),
+              LifetimesAre({{"S<int *>::S", "(a, b): a"},
+                            {"ConstructorCastSyntax", "a -> a"},
+                            {"ConstructTemporarySyntax", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStructTemporaryInitializerList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    struct S {
+      T a;
+    };
+    S<int*> InitListExpr(int* a) {
+      return {a};
+    }
+    S<int*> CastWithInitListExpr(int* a) {
+      return S<int*>{a};
+    }
+  )"),
+              LifetimesAre({{"InitListExpr", "a -> a"},
+                            {"CastWithInitListExpr", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnUnionTemporaryInitializerList) {
+  EXPECT_THAT(GetLifetimes(R"(
+    template <typename T>
+    union S {
+      T a;
+    };
+    S<int*> target(int* a) {
+      return {a};
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/recursion.cc b/lifetime_analysis/test/recursion.cc
new file mode 100644
index 0000000..c8ebe2e
--- /dev/null
+++ b/lifetime_analysis/test/recursion.cc
@@ -0,0 +1,78 @@
+// 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 involving recursion.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, InfiniteDirectRecursion) {
+  // TODO(danakj): Infinite recursion is UB, so we would like to avoid that we
+  // call an opaque function that is able to break the recursion (by exiting the
+  // program, theoretically).
+  EXPECT_THAT(GetLifetimes(R"(
+    void opaque();
+    int* f(int* a) {
+      // TODO(danakj): opaque();
+      return f(a);
+    }
+  )"),
+              LifetimesAre({{"f", "a -> b"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FiniteDirectRecursion_1Pointee) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int n, int* a) {
+      if (n <= 0) return a;
+      return f(n - 1, a);
+    }
+  )"),
+              LifetimesAre({{"f", "(), a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FiniteDirectRecursion_2Pointees) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int n, int* a, int* b) {
+      if (n <= 0) return a;
+      return f(n - 1, b, a);
+    }
+  )"),
+              LifetimesAre({{"f", "(), a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FiniteDirectRecursion_3Pointees) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* f(int n, int* a, int* b, int *c) {
+      if (n <= 0) return a;
+      return f(n - 1, b, c, a);
+    }
+  )"),
+              LifetimesAre({{"f", "(), a, a, a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MutualFiniteRecursion) {
+  EXPECT_THAT(GetLifetimes(R"(
+    int* g(int n, int* a);
+    int* f(int n, int* a) {
+      if (n == 0) return a;
+      return g(n - 1, a);
+    }
+    int* g(int n, int* a) {
+      if (n == 0) return a;
+      return f(n - 1, a);
+    }
+  )"),
+              LifetimesAre({{"f", "(), a -> a"}, {"g", "(), a -> a"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/static_lifetime.cc b/lifetime_analysis/test/static_lifetime.cc
new file mode 100644
index 0000000..8d18583
--- /dev/null
+++ b/lifetime_analysis/test/static_lifetime.cc
@@ -0,0 +1,237 @@
+// 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 involving static lifetimes.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, MaybeReturnStatic) {
+  // Check that we don't infer 'static for the parameter or the return value,
+  // which would be overly restrictive.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    int* target(int* i_non_static) {
+      if (*i_non_static > 0) {
+        return i_non_static;
+      } else {
+        static int i_static;
+        return &i_static;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MaybeReturnStaticConst) {
+  // Same as above, but return a pointer-to-const. This should have no
+  // influence on the outcome.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    const int* target(int* i_non_static) {
+      if (*i_non_static > 0) {
+        return i_non_static;
+      } else {
+        static int i_static;
+        return &i_static;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, StaticPointerOutParam) {
+  // TODO(mboehme): The lifetimes inferred here are overly restrictive. The
+  // function doesn't require the input that is passed in to have static
+  // lifetime, so it shouldn't enforce this condition on the caller. The
+  // lifetimes should be (a, b), and this would still allow the caller to
+  // substitute `static for a if desired.
+  // The root of the issue is that when we see a static lifetime in a points-to
+  // set, we don't know whether that means that
+  // - The pointer happens to point to something with static lifetime, but
+  //   nothing is depending on that, or
+  // - The pointer is required to point to something with static lifetime.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    void f(int** p) {
+      static int i = 42;
+      *p = &i;
+    }
+  )"),
+              LifetimesAre({{"f", "(static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MaybeReturnStaticStruct) {
+  // We infer a `static` lifetime parameter for `s_static` because any pointers
+  // contained in it need to outlive the struct itself. This implies that the
+  // lifetime parameter for the return value also needs to be `static`, and
+  // hence the lifetime parameter on `*s_input` needs to be `static` too.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** pp;
+    };
+    S* target(S* s_input) {
+      if (**s_input->pp > 0) {
+        return s_input;
+      } else {
+        static S s_static;
+        return &s_static;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "(static, a) -> (static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MaybeReturnStaticStructConst) {
+  // Same as above, but return a pointer-to-const. This shouldn't affect the
+  // result, as it's still possible to modify `*s.pp` even if for a `const S s`.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a", "a")]]
+      int** pp;
+    };
+    const S* target(S* s_input) {
+      if (**s_input->pp > 0) {
+        return s_input;
+      } else {
+        static S s_static;
+        return &s_static;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "(static, a) -> (static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, MaybeReturnStaticStructConstWithoutPointer) {
+  // Same as above, but with a struct that doesn't actually contain any
+  // pointers. This changes the result, as a 'static struct without any pointer
+  // can be used in place of a struct of the same type of any lifetime.
+
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      int i;
+    };
+    const S* target(S* s_input) {
+      if (s_input->i > 0) {
+        return s_input;
+      } else {
+        static S s_static;
+        return &s_static;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStaticDoublePointerWithConditional) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* p;
+    };
+    int** target(int** pp1, int** pp2) {
+      // Force *pp1 to have static lifetime.
+      static S s;
+      s.p = *pp1;
+
+      if (**pp1 > 0) {
+        return pp1;
+      } else {
+        return pp2;
+      }
+    }
+  )"),
+      LifetimesAre({{"target", "(static, a), (static, a) -> (static, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ReturnStaticConstDoublePointerWithConditional) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct [[clang::annotate("lifetime_params", "a")]] S {
+      [[clang::annotate("member_lifetimes", "a")]]
+      int* p;
+    };
+    int* const * target(int** pp1, int** pp2) {
+      // Force *pp1 to have static lifetime.
+      static S s;
+      s.p = *pp1;
+
+      if (**pp1 > 0) {
+        return pp1;
+      } else {
+        return pp2;
+      }
+    }
+  )"),
+              LifetimesAre({{"target", "(static, a), (b, a) -> (b, a)"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorStoresThisPointerInStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S() {
+        static S* last_constructed = this;
+      }
+    };
+  )"),
+              // Because S() stores the `this` pointer in a static variable, the
+              // lifetime of the `this` pointer needs to be static. This means
+              // that any instances of `S` that are constructed need to have
+              // static lifetime.
+              LifetimesAre({{"S::S", "static:"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, ConstructorStoresThisPointerInStatic_WithField) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S() {
+        static S* last_constructed = this;
+      }
+    };
+    struct T {
+      // Ensure that T() isn't defaulted because we don't want to trigger the
+      // special logic for defaulted functions.
+      T() {}
+      S s;
+    };
+  )"),
+              // TODO(b/230725905): The lifetimes for T::T should be "static:"
+              // because T contains a member variable of type S, and all
+              // instances of S need to be static.
+              LifetimesAre({{"S::S", "static:"}, {"T::T", "a:"}}));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       ConstructorStoresThisPointerInStatic_WithDerivedClass) {
+  EXPECT_THAT(GetLifetimes(R"(
+    struct S {
+      S() {
+        static S* last_constructed = this;
+      }
+    };
+    struct T : public S {
+      // Ensure that T() isn't defaulted because we don't want to trigger the
+      // special logic for defaulted functions.
+      T() {}
+    };
+  )"),
+              // TODO(b/230725905): The lifetimes for T::T should be "static:"
+              // because T derives from S and all instances of S need to be
+              // static.
+              LifetimesAre({{"S::S", "static:"}, {"T::T", "a:"}}));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/test/virtual_functions.cc b/lifetime_analysis/test/virtual_functions.cc
new file mode 100644
index 0000000..78eb244
--- /dev/null
+++ b/lifetime_analysis/test/virtual_functions.cc
@@ -0,0 +1,265 @@
+// 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 involving lifetime propagation between virtual functions.
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/test/lifetime_analysis_test.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+TEST_F(LifetimeAnalysisTest, WithPureVirtual) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a) = 0;
+};
+
+struct Derived : public Base {
+  int* f(int* a) override { return a; }
+};
+  )"),
+              LifetimesContain(
+                  {{"Base::f", "b: a -> a"}, {"Derived::f", "b: a -> a"}}));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a) = 0;
+};
+
+struct Derived1 : public Base {
+  int* f(int* a) override { return a; }
+};
+
+struct Derived2 : public Base {
+  int* f(int* a) override {
+    static int i = 42;
+    return &i;
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a -> a"},
+                  {"Derived1::f", "b: a -> a"},
+                  {"Derived2::f", "b: a -> static"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithTwoDeriveds) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a, int* b) = 0;
+};
+
+struct Derived1 : public Base {
+  int* f(int* a, int* b) override { return a; }
+};
+
+struct Derived2 : public Base {
+  int* f(int* a, int* b) override { return b; }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a, a -> a"},
+                  {"Derived1::f", "c: a, b -> a"},
+                  {"Derived2::f", "c: a, b -> b"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithBaseReturnStatic) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a) { return a; }
+};
+
+struct Derived : public Base {
+  int* f(int* a) override {
+    static int i = 42;
+    return &i;
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a -> a"},
+                  {"Derived::f", "b: a -> static"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceChained) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a) = 0;
+};
+
+struct Derived1 : public Base {
+  int* f(int* a) override {
+    static int i = 42;
+    return &i;
+  }
+};
+
+struct Derived2 : public Derived1 {
+  int* f(int* a) override { return a; }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a -> a"},
+                  {"Derived1::f", "b: a -> a"},
+                  {"Derived2::f", "b: a -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithControlFlow) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a, int* b) { return a; }
+};
+
+struct Derived : public Base {
+  int* f(int* a, int* b) override {
+    if (*a < *b)
+      return a;
+    return b;
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a, a -> a"},
+                  {"Derived::f", "b: a, a -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithLocal) {
+  EXPECT_THAT(
+      GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a) { return a; }
+};
+
+struct Derived : public Base {
+  int* f(int* a) override {
+    int i = 42;
+    return &i;
+  }
+};
+  )"),
+      LifetimesContain({
+          {"Base::f", "b: a -> a"},
+          {"Derived::f", "ERROR: function returns reference to a local"},
+      }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithStaticPtr) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual void f(int** a) {}
+};
+
+struct Derived : public Base {
+  void f(int** a) override {
+    static int i = 42;
+    *a = &i;
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: (static, a)"},
+                  {"Derived::f", "b: (static, a)"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithRecursion) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a, int* b) { return a; }
+};
+
+struct Derived : public Base {
+  int* f(int* a, int* b) override {
+    if (*a > *b)
+      return b;
+    *a -= 1;
+    return f(a, b);
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a, a -> a"},
+                  {"Derived::f", "c: a, b -> b"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest, FunctionVirtualInheritanceWithExplicitBaseCall) {
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a, int* b) { return a; }
+};
+
+struct Derived : public Base {
+  int* f(int* a, int* b) override {
+    if (*a > *b)
+      return b;
+    return Base::f(a, b);
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a, a -> a"},
+                  {"Derived::f", "b: a, a -> a"},
+              }));
+}
+
+TEST_F(LifetimeAnalysisTest,
+       DISABLED_FunctionVirtualInheritanceWithComplexRecursion) {
+  // TODO(kinuko): Fix this. Currently this doesn't work because in
+  // AnalyzeFunctionRecursive() the recursion cycle check
+  // (FindAndMarkCycleWithFunc) happens before the code expands the possible
+  // overrides, and let it return early when it finds f() in Base::f() even if
+  // it has overrides. Later in AnalyzeRecursiveFunctions Base::f() is analyzed
+  // but it doesn't expand the overrides there. See the TODO in
+  // AnalyzeFunctionRecursive.
+  EXPECT_THAT(GetLifetimes(R"(
+struct Base {
+  virtual ~Base() {}
+  virtual int* f(int* a, int* b) {
+    if (*a > *b)
+      return b;
+    *a -= 1;
+    return f(a, b);
+  }
+};
+
+struct Derived : public Base {
+  int* f(int* a, int* b) override {
+    if (*a == *b)
+      return a;
+    return Base::f(a, b);
+  }
+};
+  )"),
+              LifetimesContain({
+                  {"Base::f", "b: a, a -> a"},
+                  {"Derived::f", "b: a, a -> a"},
+              }));
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang