Open-source lifetime inference/verification code.

PiperOrigin-RevId: 450954978
diff --git a/bazel/llvm.bzl b/bazel/llvm.bzl
index bcfd0e4..47e6273 100644
--- a/bazel/llvm.bzl
+++ b/bazel/llvm.bzl
@@ -2,7 +2,7 @@
 # Exceptions. See /LICENSE for license information.
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
-load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+load("@bazel_tools//tools/build_defs/repo:git.bzl", "new_git_repository")
 
 # Create a loader/trampoline repository that we can call into to load LLVM.
 #
@@ -58,12 +58,12 @@
 def llvm_loader_repository_dependencies():
     # This *declares* the dependency, but it won't actually be *downloaded*
     # unless it's used.
-    http_archive(
+    new_git_repository(
         name = "llvm-raw",
         build_file_content = "# empty",
-        sha256 = "eb7437b60a6f78e7910d08911975f100e99e9c714f069a5487119c7eadc79171",
-        strip_prefix = "llvm-project-llvmorg-14.0.0",
-        urls = ["https://github.com/llvm/llvm-project/archive/refs/tags/llvmorg-14.0.0.zip"],
+        commit = "llvmorg-15-init-10717-ge00cbbec",
+        shallow_since = "2022-05-18",
+        remote = "https://github.com/llvm/llvm-project.git",
     )
 
 llvm_loader_repository = repository_rule(
diff --git a/lifetime_analysis/BUILD b/lifetime_analysis/BUILD
new file mode 100644
index 0000000..79d32d1
--- /dev/null
+++ b/lifetime_analysis/BUILD
@@ -0,0 +1,214 @@
+# C++ lifetime inference and verification through static analysis
+
+package(default_visibility = ["//visibility:public"])
+
+cc_library(
+    name = "analyze",
+    srcs = ["analyze.cc"],
+    hdrs = ["analyze.h"],
+    deps = [
+        ":lifetime_analysis",
+        ":lifetime_lattice",
+        ":object_repository",
+        ":template_placeholder_support",
+        ":visit_lifetimes",
+        "@absl//absl/strings",
+        "@absl//absl/strings:str_format",
+        "//lifetime_annotations",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations:lifetime_substitutions",
+        "//lifetime_annotations:pointee_type",
+        "//lifetime_annotations:type_lifetimes",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//clang:index",
+        "@llvm-project//clang:lex",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "template_placeholder_support",
+    srcs = ["template_placeholder_support.cc"],
+    hdrs = ["template_placeholder_support.h"],
+    deps = [
+        "@absl//absl/strings",
+        "@absl//absl/strings:str_format",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+        "@llvm-project//clang:lex",
+        "@llvm-project//clang:tooling",
+        "@llvm-project//clang:transformer",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "builtin_lifetimes",
+    srcs = ["builtin_lifetimes.cc"],
+    hdrs = ["builtin_lifetimes.h"],
+    deps = [
+        "@absl//absl/strings",
+        "//lifetime_annotations",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations:type_lifetimes",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:basic",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "lifetime_analysis",
+    srcs = ["lifetime_analysis.cc"],
+    hdrs = ["lifetime_analysis.h"],
+    deps = [
+        ":builtin_lifetimes",
+        ":lifetime_lattice",
+        ":object",
+        ":object_repository",
+        ":object_set",
+        ":pointer_compatibility",
+        ":points_to_map",
+        ":visit_lifetimes",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations:pointee_type",
+        "//lifetime_annotations:type_lifetimes",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "lifetime_lattice",
+    srcs = ["lifetime_lattice.cc"],
+    hdrs = ["lifetime_lattice.h"],
+    deps = [
+        ":points_to_map",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "object",
+    srcs = ["object.cc"],
+    hdrs = ["object.h"],
+    deps = [
+        "@absl//absl/strings",
+        "//lifetime_annotations:lifetime",
+        "@llvm-project//clang:ast",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "object_set",
+    srcs = ["object_set.cc"],
+    hdrs = ["object_set.h"],
+    deps = [
+        ":object",
+        "@absl//absl/strings",
+        "@absl//absl/strings:str_format",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "object_set_test",
+    srcs = ["object_set_test.cc"],
+    deps = [
+        ":object",
+        ":object_set",
+        "@com_google_googletest//:gtest_main",
+        "//lifetime_annotations",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations/test:run_on_code",
+        "@llvm-project//clang:analysis",
+    ],
+)
+
+cc_library(
+    name = "points_to_map",
+    srcs = ["points_to_map.cc"],
+    hdrs = ["points_to_map.h"],
+    deps = [
+        ":object_set",
+        "@absl//absl/strings",
+        "@absl//absl/strings:str_format",
+        "@llvm-project//clang:ast",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_test(
+    name = "points_to_map_test",
+    srcs = ["points_to_map_test.cc"],
+    deps = [
+        ":points_to_map",
+        "@com_google_googletest//:gtest_main",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations/test:run_on_code",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+    ],
+)
+
+cc_library(
+    name = "object_repository",
+    srcs = ["object_repository.cc"],
+    hdrs = ["object_repository.h"],
+    deps = [
+        ":object",
+        ":object_set",
+        ":points_to_map",
+        ":visit_lifetimes",
+        "//lifetime_annotations:lifetime",
+        "//lifetime_annotations:pointee_type",
+        "//lifetime_annotations:type_lifetimes",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:basic",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "visit_lifetimes",
+    srcs = ["visit_lifetimes.cc"],
+    hdrs = ["visit_lifetimes.h"],
+    deps = [
+        ":object",
+        ":object_set",
+        "//lifetime_annotations:pointee_type",
+        "//lifetime_annotations:type_lifetimes",
+        "@llvm-project//clang:ast",
+        "@llvm-project//llvm:Support",
+    ],
+)
+
+cc_library(
+    name = "pointer_compatibility",
+    srcs = ["pointer_compatibility.cc"],
+    hdrs = ["pointer_compatibility.h"],
+    deps = [
+        "//lifetime_annotations:pointee_type",
+        "@llvm-project//clang:ast",
+    ],
+)
+
+cc_test(
+    name = "pointer_compatibility_test",
+    srcs = ["pointer_compatibility_test.cc"],
+    deps = [
+        ":pointer_compatibility",
+        "@com_google_googletest//:gtest_main",
+        "//lifetime_annotations",
+        "//lifetime_annotations/test:run_on_code",
+        "@llvm-project//clang:analysis",
+        "@llvm-project//clang:ast",
+        "@llvm-project//clang:ast_matchers",
+    ],
+)
diff --git a/lifetime_analysis/README.md b/lifetime_analysis/README.md
new file mode 100644
index 0000000..89c5eff
--- /dev/null
+++ b/lifetime_analysis/README.md
@@ -0,0 +1,5 @@
+# C++ lifetime inference and verification through static analysis
+
+This package contains a prototype for a static analysis tool that infers
+and verifies lifetime annotations for C++ code. For more background, see
+<internal link>.
diff --git a/lifetime_analysis/analyze.cc b/lifetime_analysis/analyze.cc
new file mode 100644
index 0000000..9dab67a
--- /dev/null
+++ b/lifetime_analysis/analyze.cc
@@ -0,0 +1,1648 @@
+// 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/analyze.h"
+
+#include <algorithm>
+#include <map>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/str_replace.h"
+#include "lifetime_analysis/lifetime_analysis.h"
+#include "lifetime_analysis/lifetime_lattice.h"
+#include "lifetime_analysis/object_repository.h"
+#include "lifetime_analysis/template_placeholder_support.h"
+#include "lifetime_analysis/visit_lifetimes.h"
+#include "lifetime_annotations/function_lifetimes.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/lifetime_substitutions.h"
+#include "lifetime_annotations/pointee_type.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DeclTemplate.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Type.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/CFG.h"
+#include "clang/Analysis/FlowSensitive/ControlFlowContext.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/DataflowEnvironment.h"
+#include "clang/Analysis/FlowSensitive/WatchedLiteralsSolver.h"
+#include "clang/Index/USRGeneration.h"
+#include "clang/Lex/Lexer.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/SmallPtrSet.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+struct VisitedCallStackEntry {
+  const clang::FunctionDecl* func;
+  bool in_cycle;
+  bool in_overrides_traversal;
+};
+
+// A map from base methods to overriding methods.
+using BaseToOverrides =
+    llvm::DenseMap<const clang::CXXMethodDecl*,
+                   llvm::SmallPtrSet<const clang::CXXMethodDecl*, 2>>;
+
+// Enforce the invariant that an object of static lifetime should only point at
+// other objects of static lifetime.
+void PropagateStaticToPointees(LifetimeSubstitutions& subst,
+                               const PointsToMap& points_to_map) {
+  std::vector<Object> pointees =
+      points_to_map.GetAllPointersWithLifetime(Lifetime::Static());
+
+  llvm::DenseSet<Object> visited;
+
+  while (!pointees.empty()) {
+    Object cur = pointees.back();
+    pointees.pop_back();
+    visited.insert(cur);
+    if (cur.GetLifetime() != Lifetime::Static()) {
+      subst.Add(cur.GetLifetime(), Lifetime::Static());
+    }
+
+    for (Object pointee : points_to_map.GetPointerPointsToSet(cur)) {
+      if (!visited.count(pointee)) {
+        pointees.push_back(pointee);
+      }
+    }
+  }
+}
+
+// DO NOT use this function on untrusted input.
+// TODO(veluca): ideally, this function should be replaced with one from a
+// fuzzed library. However, as the way it is used doesn't have significant
+// security implications (its input is trusted, coming from tests, and its
+// output is unused except sometimes to produce a graphviz .dot file), and as
+// the logic for HTML escaping is simple enough, this function is reasonable to
+// use here.
+std::string EscapeHtmlChars(absl::string_view input) {
+  std::string escaped;
+  escaped.reserve(input.size());
+  for (auto c : input) {
+    switch (c) {
+      case '\'':
+        escaped += "&#39;";
+        break;
+      case '"':
+        escaped += "&quot;";
+        break;
+      case '<':
+        escaped += "&lt;";
+        break;
+      case '>':
+        escaped += "&gt;";
+        break;
+      case '&':
+        escaped += "&amp;";
+        break;
+      default:
+        escaped += c;
+    }
+  }
+  return escaped;
+}
+
+std::string VariableLabel(absl::string_view name, Object object) {
+  return absl::StrFormat("<<b>%s</b> (%s)>", EscapeHtmlChars(name),
+                         EscapeHtmlChars(object.DebugString()));
+}
+
+std::string PointsToEdgesDot(const ObjectRepository& object_repository,
+                             const PointsToMap& points_to_map,
+                             absl::string_view name_prefix) {
+  std::vector<std::string> lines;
+  llvm::DenseSet<Object> all_objects, var_objects;
+
+  for (auto [pointer, points_to_set] : points_to_map.PointerPointsTos()) {
+    all_objects.insert(pointer);
+    for (auto points_to : points_to_set) {
+      all_objects.insert(points_to);
+      lines.push_back(absl::StrFormat(R"("%1$s%2$s" -> "%1$s%3$s")",
+                                      name_prefix, pointer.DebugString(),
+                                      points_to.DebugString()));
+    }
+  }
+
+  for (auto [key, field_object] : object_repository.GetFieldObjects()) {
+    auto [struct_object, field] = key;
+    lines.push_back(absl::StrFormat(
+        R"("%1$s%2$s" -> "%1$s%3$s" [style=dashed label="%4$s"])", name_prefix,
+        struct_object.DebugString(), field_object.DebugString(),
+        field->getNameAsString()));
+  }
+
+  for (auto [key, base_object] : object_repository.GetBaseObjects()) {
+    auto [struct_object, base] = key;
+    lines.push_back(absl::StrFormat(
+        R"("%1$s%2$s" -> "%1$s%3$s" [style=dashed label="%4$s"])", name_prefix,
+        struct_object.DebugString(), base_object.DebugString(),
+        clang::QualType(base, 0).getAsString()));
+  }
+
+  if (object_repository.GetThisObject().has_value()) {
+    var_objects.insert(*object_repository.GetThisObject());
+    lines.push_back(absl::StrFormat(
+        "\"%s%s\"[label=%s]", name_prefix,
+        object_repository.GetThisObject()->DebugString(),
+        VariableLabel("this", *object_repository.GetThisObject())));
+  }
+
+  for (auto [decl, object] : object_repository) {
+    var_objects.insert(object);
+    lines.push_back(
+        absl::StrFormat("\"%s%s\"[label=%s]", name_prefix, object.DebugString(),
+                        VariableLabel(decl->getNameAsString(), object)));
+  }
+
+  var_objects.insert(object_repository.GetReturnObject());
+  lines.push_back(absl::StrFormat(
+      "\"%s%s\"[label=%s]", name_prefix,
+      object_repository.GetReturnObject().DebugString(),
+      VariableLabel("return", object_repository.GetReturnObject())));
+
+  for (Object object : all_objects) {
+    if (!var_objects.contains(object)) {
+      lines.push_back(absl::StrFormat(R"("%1$s%2$s"[label="%2$s"])",
+                                      name_prefix, object.DebugString()));
+    }
+  }
+
+  for (auto [_, object] : object_repository.GetFieldObjects()) {
+    if (!var_objects.contains(object)) {
+      lines.push_back(absl::StrFormat(R"("%1$s%2$s"[label="%2$s"])",
+                                      name_prefix, object.DebugString()));
+    }
+  }
+
+  for (auto [_, object] : object_repository.GetBaseObjects()) {
+    if (!var_objects.contains(object)) {
+      lines.push_back(absl::StrFormat(R"("%1$s%2$s"[label="%2$s"])",
+                                      name_prefix,
+                                      VariableLabel("this", object)));
+    }
+  }
+
+  lines.push_back("");
+
+  return absl::StrJoin(lines, ";\n");
+}
+
+std::string PointsToGraphDot(const ObjectRepository& object_repository,
+                             const PointsToMap& points_to_map) {
+  return absl::StrCat("digraph d {\n",
+                      PointsToEdgesDot(object_repository, points_to_map, ""),
+                      "}");
+}
+
+std::string CfgBlockLabel(const clang::CFGBlock* block, const clang::CFG& cfg,
+                          const clang::ASTContext& ast_context) {
+  std::string block_name = absl::StrCat("B", block->getBlockID());
+  if (block == &cfg.getEntry()) {
+    absl::StrAppend(&block_name, " (ENTRY)");
+  } else if (block == &cfg.getExit()) {
+    absl::StrAppend(&block_name, " (EXIT)");
+  }
+
+  std::string label =
+      absl::StrFormat("<tr><td>%s</td></tr>", EscapeHtmlChars(block_name));
+
+  clang::SourceRange range;
+  for (const auto& element : *block) {
+    if (auto cfg_stmt = element.getAs<clang::CFGStmt>()) {
+      clang::SourceRange stmt_range = cfg_stmt->getStmt()->getSourceRange();
+      if (range.isInvalid()) {
+        range = stmt_range;
+      } else {
+        if (stmt_range.getBegin() < range.getBegin()) {
+          range.setBegin(stmt_range.getBegin());
+        }
+        if (stmt_range.getEnd() > range.getEnd()) {
+          range.setEnd(stmt_range.getEnd());
+        }
+      }
+    }
+  }
+
+  if (range.isValid()) {
+    const clang::SourceManager& source_manager = ast_context.getSourceManager();
+    clang::StringRef filename = source_manager.getFilename(range.getBegin());
+    unsigned line_begin =
+        source_manager.getSpellingLineNumber(range.getBegin());
+    unsigned col_begin =
+        source_manager.getSpellingColumnNumber(range.getBegin());
+    unsigned line_end = source_manager.getSpellingLineNumber(range.getEnd());
+    unsigned col_end = source_manager.getSpellingColumnNumber(range.getEnd());
+
+    absl::StrAppendFormat(&label, "<tr><td>%s:%u:%u-%u:%u</td></tr>",
+                          EscapeHtmlChars(filename.str()), line_begin,
+                          col_begin, line_end, col_end);
+
+    absl::StrAppendFormat(
+        &label, "<tr><td>%s</td></tr>",
+        EscapeHtmlChars(clang::Lexer::getSourceText(
+                            clang::CharSourceRange::getTokenRange(range),
+                            source_manager, ast_context.getLangOpts())
+                            .str()));
+  }
+
+  return absl::StrFormat("<<table border=\"0\">%s</table>>", label);
+}
+
+std::string CreateCfgDot(
+    const clang::CFG& cfg, const clang::ASTContext& ast_context,
+    const std::vector<llvm::Optional<
+        clang::dataflow::DataflowAnalysisState<LifetimeLattice>>>&
+        block_to_output_state,
+    const ObjectRepository& object_repository) {
+  std::string result = "digraph d {\ncompound=true;\nedge [minlen=2];\n";
+
+  for (const clang::CFGBlock* block : cfg) {
+    unsigned id = block->getBlockID();
+
+    absl::StrAppendFormat(&result, "subgraph cluster%u {\n", id);
+
+    absl::StrAppendFormat(&result, "label=%s;\n",
+                          CfgBlockLabel(block, cfg, ast_context));
+
+    absl::StrAppend(&result, "{\nrank=source;\n");
+    absl::StrAppendFormat(
+        &result,
+        "B%usource [style=\"invis\",width=0,height=0,fixedsize=true];\n", id);
+    absl::StrAppend(&result, "}\n");
+    absl::StrAppend(&result, "{\nrank=sink;\n");
+    absl::StrAppendFormat(
+        &result, "B%usink [style=\"invis\",width=0,height=0,fixedsize=true];\n",
+        id);
+    absl::StrAppend(&result, "}\n");
+
+    const auto block_state = block_to_output_state.at(id);
+    if (block_state) {
+      auto lattice = block_state->Lattice;
+      if (!lattice.IsError()) {
+        absl::StrAppend(&result,
+                        PointsToEdgesDot(object_repository, lattice.PointsTo(),
+                                         absl::StrCat("B", id, "_")));
+      }
+    }
+
+    absl::StrAppend(&result, "}\n");
+  }
+
+  for (const clang::CFGBlock* block : cfg) {
+    for (const clang::CFGBlock* succ : block->succs()) {
+      absl::StrAppendFormat(
+          &result,
+          "B%1$usink -> B%2$usource [ltail=cluster%1$u,lhead=cluster%2$u];\n",
+          block->getBlockID(), succ->getBlockID());
+    }
+  }
+
+  absl::StrAppend(&result, "}");
+
+  return result;
+}
+
+// Reduces a set of lifetimes to a single lifetime such that all lifetimes can
+// be returned as that single lifetime. This generally requires substituting
+// variable lifetimes by that single lifetime; these substitutions are added to
+// `subst`.
+// The exact behavior depends on whether the reference-like type being returned
+// is in covariant or invariant position, as specified by `variance`:
+// - `kCovariant`: All lifetimes in the input set outlive the returned lifetime.
+// - `kInvariant`: All lifetimes in the input set are identical to the returned
+//   lifetime (after substitution).
+Lifetime UnifyLifetimes(llvm::SmallSet<Lifetime, 2> lifetimes,
+                        Variance variance, LifetimeSubstitutions& subst) {
+  assert(!lifetimes.empty());
+
+  // Simple case: If there's only one lifetime, return that.
+  if (lifetimes.size() == 1) {
+    return *lifetimes.begin();
+  }
+
+  // 'local is outlived by all other lifetimes, so if we have a local in the
+  // set, just return that.
+  // There are some cases in which this doesn't strictly return a correct result
+  // in the sense that all input lifetimes can be converted to this local
+  // lifetime, namely if there are multiple local lifetimes in the set, or
+  // if `variance == kInvariant` and `lifetimes` contains both a local lifetime
+  // and the static lifetime. However, doesn't really matter beacuse we always
+  // treat returning a local lifetime as an error anyway.
+  for (Lifetime lifetime : lifetimes) {
+    if (lifetime.IsLocal()) {
+      return lifetime;
+    }
+  }
+
+  // Lifetime to substitute all others by. Initially, just pick an arbitrary
+  // lifetime.
+  Lifetime result = *lifetimes.begin();
+
+  if (lifetimes.contains(Lifetime::Static())) {
+    switch (variance) {
+      case kInvariant:
+        // Have to substitute all other lifetimes by 'static.
+        result = Lifetime::Static();
+        break;
+      case kCovariant:
+        // Ignore 'static, as it outlives all other lifetimes in the set (and we
+        // know that it's not the only lifetime).
+        lifetimes.erase(Lifetime::Static());
+        // `result` might previously have been 'static, so pick a new lifetime.
+        result = *lifetimes.begin();
+        break;
+    }
+  }
+
+  // Substitute all other lifetimes by the chosen lifetime.
+  for (Lifetime l : lifetimes) {
+    if (l != result) {
+      subst.Add(l, result);
+    }
+  }
+
+  return result;
+}
+
+void FindLifetimeSubstitutions(Object root_object, clang::QualType type,
+                               const PointsToMap& points_to_map,
+                               const ObjectRepository& object_repository,
+                               const ValueLifetimes& value_lifetimes,
+                               LifetimeSubstitutions& subst) {
+  class Visitor : public LifetimeVisitor {
+   public:
+    Visitor(const ObjectRepository& object_repository,
+            const PointsToMap& points_to_map, LifetimeSubstitutions& subst)
+        : object_repository_(object_repository),
+          points_to_map_(points_to_map),
+          subst_(subst) {}
+
+    Object GetFieldObject(const ObjectSet& objects,
+                          const clang::FieldDecl* field) override {
+      // All the objects have the same field.
+      assert(!objects.empty());
+      return object_repository_.GetFieldObject(*objects.begin(), field);
+    }
+
+    Object GetBaseClassObject(const ObjectSet& objects,
+                              clang::QualType base) override {
+      // All the objects have the same base.
+      assert(!objects.empty());
+      return object_repository_.GetBaseClassObject(*objects.begin(), base);
+    }
+
+    ObjectSet Traverse(const ObjectLifetimes& lifetimes,
+                       const ObjectSet& objects, int pointee_depth) override {
+      ObjectSet child_pointees = points_to_map_.GetPointerPointsToSet(objects);
+      if (child_pointees.empty()) return child_pointees;
+      if (PointeeType(lifetimes.GetValueLifetimes().Type()).isNull())
+        return child_pointees;
+
+      Variance variance = kCovariant;
+
+      // Non-const reference-like type: the lifetime of objects it points to
+      // appear in invariant position; the root pointee (the pointee to the
+      // local variable) never causes its pointed-to-elements to be considered
+      // to appear in an invariant position.
+      if (!lifetimes.GetValueLifetimes().Type().isConstQualified() &&
+          pointee_depth != 0) {
+        variance = kInvariant;
+      }
+      llvm::SmallSet<Lifetime, 2> pointee_lifetimes;
+      for (Object pointee : child_pointees) {
+        pointee_lifetimes.insert(subst_.Substitute(pointee.GetLifetime()));
+      }
+      assert(!pointee_lifetimes.empty());
+      if (!lifetimes.GetValueLifetimes()
+               .GetPointeeLifetimes()
+               .GetLifetime()
+               .IsLocal()) {
+        subst_.Add(
+            lifetimes.GetValueLifetimes().GetPointeeLifetimes().GetLifetime(),
+            UnifyLifetimes(pointee_lifetimes, variance, subst_));
+      }
+      return child_pointees;
+    }
+
+   private:
+    const ObjectRepository& object_repository_;
+    const PointsToMap& points_to_map_;
+    LifetimeSubstitutions& subst_;
+  };
+
+  Visitor visitor(object_repository, points_to_map, subst);
+  // Since we run our visit starting from the object representing the local
+  // variable, we create the corresponding ObjectLifetimes.
+  VisitLifetimes({root_object}, type,
+                 ObjectLifetimes(root_object.GetLifetime(), value_lifetimes),
+                 visitor);
+}
+
+// TODO(veluca): this really ought to happen in the dataflow framework/CFG, but
+// at the moment only the *expressions* in initializers get added, not
+// initialization itself.
+void ExtendPointsToMapWithInitializers(
+    const clang::CXXConstructorDecl* constructor,
+    const ObjectRepository& object_repository, PointsToMap& points_to_map) {
+  auto this_object = object_repository.GetThisObject();
+  if (!this_object.has_value()) {
+    assert(false);
+    return;
+  }
+  for (const auto* init : constructor->inits()) {
+    if (!init->isAnyMemberInitializer()) continue;
+    const clang::FieldDecl* field = init->getMember();
+    const auto* init_expr = init->getInit();
+    if (clang::isa<clang::CXXDefaultInitExpr>(init_expr)) {
+      init_expr = field->getInClassInitializer();
+    }
+    if (!IsInitExprInitializingARecordObject(init_expr)) {
+      TransferInitializer(
+          object_repository.GetFieldObject(this_object.value(), field),
+          field->getType(), object_repository, init_expr, points_to_map);
+    }
+  }
+}
+
+// Modifies the given substitutions to update the `target` lifetime to the
+// lifetime that would be more constraining between `base` and `constraining`,
+// updating `is_more_constraining` to inform about whether the final function
+// lifetimes will be non-isomorphic to the ones originally in `base`.
+void MergeLifetimes(Lifetime target, Lifetime base, Lifetime constraining,
+                    Variance variance, LifetimeSubstitutions& subst,
+                    bool& is_more_constraining) {
+  // TODO(veluca): handle covariance.
+  assert(target.IsVariable());
+  assert(!base.IsLocal());
+  assert(!constraining.IsLocal());
+  if (base == Lifetime::Static()) {
+    if (variance == kCovariant) {
+      subst.Add(target, constraining);
+      is_more_constraining = true;
+    } else {
+      subst.Add(target, base);
+    }
+    return;
+  }
+  subst.Add(target, base);
+  if (constraining == Lifetime::Static()) {
+    if (variance == kInvariant) {
+      if (subst.Substitute(base) != base &&
+          subst.Substitute(base) != Lifetime::Static()) {
+        is_more_constraining = true;
+      }
+      subst.Add(subst.Substitute(base), Lifetime::Static());
+    }
+    return;
+  }
+  if ((subst.Substitute(base) != base &&
+       subst.Substitute(base) != constraining) ||
+      (subst.Substitute(constraining) != constraining &&
+       subst.Substitute(constraining) != base)) {
+    is_more_constraining = true;
+  }
+  subst.Add(subst.Substitute(constraining), base);
+  subst.Add(subst.Substitute(base), constraining);
+}
+
+void CollectLifetimeMapping(const ValueLifetimes&, const ValueLifetimes&,
+                            const ValueLifetimes&, Variance, Variance,
+                            LifetimeSubstitutions&, bool&);
+
+void CollectLifetimeMapping(const ObjectLifetimes& target,
+                            const ObjectLifetimes& base,
+                            const ObjectLifetimes& constraining,
+                            Variance self_variance,
+                            LifetimeSubstitutions& subst,
+                            bool& is_more_constraining) {
+  // Special case: if we have a non-const pointer, run invariant unification
+  // on the pointee.
+  Variance pointee_variance = kCovariant;
+  if (!PointeeType(base.Type()).isNull() && !base.Type().isConstQualified()) {
+    pointee_variance = kInvariant;
+  }
+  MergeLifetimes(target.GetLifetime(), base.GetLifetime(),
+                 constraining.GetLifetime(), self_variance, subst,
+                 is_more_constraining);
+  CollectLifetimeMapping(target.GetValueLifetimes(), base.GetValueLifetimes(),
+                         constraining.GetValueLifetimes(), self_variance,
+                         pointee_variance, subst, is_more_constraining);
+}
+
+void CollectLifetimeMapping(const ValueLifetimes& target,
+                            const ValueLifetimes& base,
+                            const ValueLifetimes& constraining,
+                            Variance self_variance, Variance pointee_variance,
+                            LifetimeSubstitutions& subst,
+                            bool& is_more_constraining) {
+  assert(target.Type().getCanonicalType() == base.Type().getCanonicalType());
+  assert(target.Type().getCanonicalType() ==
+         constraining.Type().getCanonicalType());
+  if (!PointeeType(base.Type()).isNull()) {
+    CollectLifetimeMapping(target.GetPointeeLifetimes(),
+                           base.GetPointeeLifetimes(),
+                           constraining.GetPointeeLifetimes(), pointee_variance,
+                           subst, is_more_constraining);
+  }
+  if (base.Type()->isRecordType()) {
+    assert(base.GetNumTemplateNestingLevels() ==
+           constraining.GetNumTemplateNestingLevels());
+    assert(base.GetNumTemplateNestingLevels() ==
+           target.GetNumTemplateNestingLevels());
+    for (size_t depth = 0; depth < base.GetNumTemplateNestingLevels();
+         depth++) {
+      assert(base.GetNumTemplateArgumentsAtDepth(depth) ==
+             constraining.GetNumTemplateArgumentsAtDepth(depth));
+      assert(base.GetNumTemplateArgumentsAtDepth(depth) ==
+             target.GetNumTemplateArgumentsAtDepth(depth));
+      for (size_t idx = 0; idx < base.GetNumTemplateArgumentsAtDepth(depth);
+           idx++) {
+        std::optional<ValueLifetimes> target_arg =
+            target.GetTemplateArgumentLifetimes(depth, idx);
+        std::optional<ValueLifetimes> base_arg =
+            base.GetTemplateArgumentLifetimes(depth, idx);
+        std::optional<ValueLifetimes> constraining_arg =
+            constraining.GetTemplateArgumentLifetimes(depth, idx);
+        assert(base_arg.has_value() == constraining_arg.has_value());
+        assert(target_arg.has_value() == constraining_arg.has_value());
+        if (target_arg.has_value() && base_arg.has_value() &&
+            constraining_arg.has_value()) {
+          CollectLifetimeMapping(*target_arg, *base_arg, *constraining_arg,
+                                 kInvariant, kInvariant, subst,
+                                 is_more_constraining);
+        }
+      }
+    }
+    for (const auto& lftm_param : GetLifetimeParameters(base.Type())) {
+      MergeLifetimes(target.GetLifetimeParameter(lftm_param),
+                     base.GetLifetimeParameter(lftm_param),
+                     constraining.GetLifetimeParameter(lftm_param),
+                     self_variance, subst, is_more_constraining);
+    }
+  }
+  // TODO(veluca): function types.
+}
+
+void CollectLifetimeMapping(const FunctionLifetimes& target,
+                            const FunctionLifetimes& base,
+                            const FunctionLifetimes& constraining,
+                            LifetimeSubstitutions& subst,
+                            bool& is_more_constraining) {
+  for (size_t i = 0; i < base.GetNumParams(); i++) {
+    CollectLifetimeMapping(target.GetParamLifetimes(i),
+                           base.GetParamLifetimes(i),
+                           constraining.GetParamLifetimes(i), kCovariant,
+                           kCovariant, subst, is_more_constraining);
+  }
+  CollectLifetimeMapping(target.GetReturnLifetimes(), base.GetReturnLifetimes(),
+                         constraining.GetReturnLifetimes(), kCovariant,
+                         kCovariant, subst, is_more_constraining);
+  if (base.IsNonStaticMethod()) {
+    CollectLifetimeMapping(target.GetThisLifetimes(), base.GetThisLifetimes(),
+                           constraining.GetThisLifetimes(), kCovariant,
+                           kCovariant, subst, is_more_constraining);
+  }
+}
+
+// Returns a pair containing the constrained lifetimes and a boolean that is
+// set to true if the lifetimes are, in fact, more constrained.
+std::pair<FunctionLifetimes, bool> ConstrainLifetimes(
+    const FunctionLifetimes& base, const FunctionLifetimes& constraining) {
+  FunctionLifetimes copy =
+      base.CreateCopy(
+              [](clang::QualType, llvm::StringRef) -> llvm::Expected<Lifetime> {
+                return Lifetime::CreateVariable();
+              })
+          .get();
+  LifetimeSubstitutions subst;
+  bool is_more_constraining = false;
+  CollectLifetimeMapping(copy, base, constraining, subst, is_more_constraining);
+  copy.SubstituteLifetimes(subst);
+  return {copy, is_more_constraining};
+}
+
+struct FunctionAnalysis {
+  ObjectRepository object_repository;
+  PointsToMap points_to_map;
+  LifetimeSubstitutions subst;
+};
+
+bool HasRecordTypeFields(const clang::RecordDecl* record) {
+  for (const clang::FieldDecl* field : record->fields()) {
+    if (field->getType()->isRecordType()) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+llvm::Expected<FunctionAnalysis> AnalyzeDefaultedFunction(
+    const clang::FunctionDecl* func,
+    const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+    /*callee_lifetimes*/) {
+  assert(func->isDefaulted());
+
+  // TODO(b/230693710): Add complete support for defaulted functions.
+
+  if (const auto* ctor = clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+    if (ctor->isDefaultConstructor()) {
+      const clang::CXXRecordDecl* record = ctor->getParent();
+      if (record->getNumBases() == 0 && !HasRecordTypeFields(record)) {
+        return FunctionAnalysis{.object_repository = ObjectRepository(func)};
+      }
+    }
+  }
+
+  return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                 "unsupported type of defaulted function");
+}
+
+llvm::Expected<FunctionAnalysis> AnalyzeSingleFunctionBody(
+    const clang::FunctionDecl* func,
+    const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        callee_lifetimes,
+    const DiagnosticReporter& diag_reporter, FunctionDebugInfoMap* debug_info) {
+  const auto* cxxmethod = clang::dyn_cast<clang::CXXMethodDecl>(func);
+  if (cxxmethod && cxxmethod->isPure()) {
+    return FunctionAnalysis{.object_repository = ObjectRepository(func)};
+  }
+
+  func = func->getDefinition();
+  assert(func != nullptr);
+
+  if (!func->getBody()) {
+    // TODO(b/230693710): Do this unconditionally for defaulted functions, even
+    // if they happen to have a body (because something caused Sema to create a
+    // body for them). We can't do this yet because we don't have full support
+    // for defaulted functions yet, so we would break tests where we happen to
+    // have a body for the defaulted function today.
+    if (func->isDefaulted()) {
+      return AnalyzeDefaultedFunction(func, callee_lifetimes);
+    }
+
+    return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                   "Declaration-only!");
+  }
+
+  auto cfctx = clang::dataflow::ControlFlowContext::build(
+      func, func->getBody(), &func->getASTContext());
+  if (!cfctx) return cfctx.takeError();
+
+  clang::dataflow::DataflowAnalysisContext analysis_context(
+      std::make_unique<clang::dataflow::WatchedLiteralsSolver>());
+  clang::dataflow::Environment environment(analysis_context);
+
+  ObjectRepository object_repository(func);
+
+  LifetimeAnalysis analysis(func, object_repository, callee_lifetimes,
+                            diag_reporter);
+
+  llvm::Expected<std::vector<
+      llvm::Optional<clang::dataflow::DataflowAnalysisState<LifetimeLattice>>>>
+      maybe_block_to_output_state =
+          clang::dataflow::runDataflowAnalysis(*cfctx, analysis, environment);
+  if (!maybe_block_to_output_state) {
+    return maybe_block_to_output_state.takeError();
+  }
+  auto& block_to_output_state = *maybe_block_to_output_state;
+
+  const auto exit_block_state =
+      block_to_output_state.at(cfctx->getCFG().getExit().getBlockID());
+  if (!exit_block_state.hasValue()) {
+    assert(false);
+    return llvm::createStringError(
+        llvm::inconvertibleErrorCode(),
+        absl::StrCat("CFG exit block for '", func->getNameAsString(),
+                     "' unexpectedly does not exist"));
+  }
+
+  auto exit_lattice = exit_block_state.getValue().Lattice;
+  if (exit_lattice.IsError()) {
+    return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                   exit_lattice.Error());
+  }
+
+  PointsToMap points_to_map = exit_lattice.PointsTo();
+
+  // Adding initializers to the PointsToMap *before* dataflow analysis is
+  // problematic because the expressions do not have a lifetime yet in the map
+  // itself.
+  // However, member access in a struct does not ever produce lifetimes that
+  // depend on what those members are initialized to - lifetimes of members
+  // (or things that members point to) are either the same as the lifetime of
+  // this, or a lifetime parameter of the struct, so processing initializers
+  // afterwards is correct.
+  if (auto* constructor = clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+    ExtendPointsToMapWithInitializers(constructor, object_repository,
+                                      points_to_map);
+  }
+
+  if (debug_info) {
+    std::string ast;
+    llvm::raw_string_ostream os(ast);
+    func->dump(os);
+    os.flush();
+    (*debug_info)[func].ast = std::move(ast);
+    (*debug_info)[func].object_repository = object_repository.DebugString();
+    (*debug_info)[func].points_to_map_dot =
+        PointsToGraphDot(object_repository, points_to_map);
+    (*debug_info)[func].cfg_dot =
+        CreateCfgDot(cfctx->getCFG(), func->getASTContext(),
+                     block_to_output_state, object_repository);
+  }
+
+  LifetimeSubstitutions subst;
+  PropagateStaticToPointees(subst, points_to_map);
+
+  return FunctionAnalysis{
+      .object_repository = std::move(object_repository),
+      .points_to_map = std::move(points_to_map),
+      .subst = std::move(subst),
+  };
+}
+
+llvm::Error DiagnoseReturnLocal(const clang::FunctionDecl* func,
+                                const FunctionLifetimes& lifetimes,
+                                const DiagnosticReporter& diag_reporter) {
+  auto contains_local = [](const ValueLifetimes& lifetimes) {
+    return lifetimes.HasAny(&Lifetime::IsLocal);
+  };
+
+  for (unsigned i = 0; i < func->getNumParams(); ++i) {
+    const clang::ParmVarDecl* param = func->getParamDecl(i);
+    if (contains_local(lifetimes.GetParamLifetimes(i))) {
+      std::string error_msg = absl::StrFormat(
+          "function returns reference to a local through parameter '%s'",
+          param->getNameAsString());
+      diag_reporter(param->getBeginLoc(), error_msg,
+                    clang::DiagnosticIDs::Error);
+      return llvm::createStringError(llvm::inconvertibleErrorCode(), error_msg);
+    }
+  }
+
+  if (const auto* method = clang::dyn_cast<clang::CXXMethodDecl>(func);
+      method && !method->isStatic() &&
+      contains_local(lifetimes.GetThisLifetimes())) {
+    std::string error_msg =
+        "function returns reference to a local through 'this'";
+    diag_reporter(func->getBeginLoc(), error_msg, clang::DiagnosticIDs::Error);
+    return llvm::createStringError(llvm::inconvertibleErrorCode(), error_msg);
+  }
+
+  if (contains_local(lifetimes.GetReturnLifetimes())) {
+    std::string error_msg = "function returns reference to a local";
+    diag_reporter(func->getBeginLoc(), error_msg, clang::DiagnosticIDs::Error);
+    return llvm::createStringError(llvm::inconvertibleErrorCode(), error_msg);
+  }
+
+  return llvm::Error::success();
+}
+
+// Constructs the FunctionLifetimes for a function, given a PointsToMap,
+// ObjectRepository, and LifetimeSubstitutions that have been built from the
+// function's body, which would include the function's parameters. It's also
+// possible to call this function with an empty inputs in order to generate
+// a FunctionLifetimes that matches the function's signature but without any
+// constraits (i.e. each lifetime that appears would be independent).
+llvm::Expected<FunctionLifetimes> ConstructFunctionLifetimes(
+    const clang::FunctionDecl* func, FunctionAnalysis analysis,
+    const DiagnosticReporter& diag_reporter) {
+  if (func->getDefinition()) {
+    func = func->getDefinition();
+  } else {
+    // This can happen only when `func` is a pure virtual method.
+    const auto* cxxmethod = clang::dyn_cast<clang::CXXMethodDecl>(func);
+    assert(cxxmethod && cxxmethod->isPure());
+    // Pure virtual member functions can only ever have a single declaration,
+    // so we know we're already looking at the canonical declaration.
+    if (++cxxmethod->redecls_begin() != cxxmethod->redecls_end()) {
+      assert(false);
+      func = func->getCanonicalDecl();
+    }
+  }
+
+  auto& [object_repository, points_to_map, subst] = analysis;
+
+  // We create "fake" lifetimes for the function, then walk the type and find
+  // out which input-to-the-function-call lifetime to use as a replacement using
+  // UnifyLifetimes.
+  FunctionLifetimeFactorySingleCallback factory(
+      [](clang::QualType, llvm::StringRef) {
+        return Lifetime::CreateVariable();
+      });
+  FunctionLifetimes result =
+      FunctionLifetimes::CreateForDecl(func, factory).get();
+
+  // For each parameter that is of reference-like type, find the lifetimes of
+  // all of its transitive pointees. At each level of indirection, unify all
+  // lifetimes in the points-to set into a single lifetime by performing
+  // appropriate substitutions.
+  for (unsigned i = 0; i < func->getNumParams(); ++i) {
+    const clang::ParmVarDecl* param = func->getParamDecl(i);
+    FindLifetimeSubstitutions(
+        object_repository.GetOriginalParameterValue(param), param->getType(),
+        points_to_map, object_repository, result.GetParamLifetimes(i), subst);
+  }
+
+  // If in a member function, handle the implicit `this` argument.
+  if (const auto* method_decl = clang::dyn_cast<clang::CXXMethodDecl>(func)) {
+    if (!method_decl->isStatic()) {
+      auto this_object = object_repository.GetThisObject();
+      if (!this_object.has_value()) {
+        assert(false);
+        return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                       "Programming logic error");
+      }
+      // `this` does not have a local variable. We magick a pointer that points
+      // to `this` anyway for consistency with the other calls.
+      Object points_to_this =
+          Object::Create(Lifetime::CreateLocal(), method_decl->getThisType());
+      points_to_map.SetPointerPointsToSet(points_to_this,
+                                          {this_object.value()});
+      FindLifetimeSubstitutions(points_to_this, method_decl->getThisType(),
+                                points_to_map, object_repository,
+                                result.GetThisLifetimes(), subst);
+    }
+  }
+
+  FindLifetimeSubstitutions(
+      object_repository.GetReturnObject(), func->getReturnType(), points_to_map,
+      object_repository, result.GetReturnLifetimes(), subst);
+
+  result.SubstituteLifetimes(subst);
+
+  if (llvm::Error err = DiagnoseReturnLocal(func, result, diag_reporter)) {
+    return std::move(err);
+  }
+
+  return result;
+}
+
+llvm::Expected<llvm::DenseSet<const clang::FunctionDecl*>>
+GetDefaultedFunctionCallees(const clang::FunctionDecl* func) {
+  assert(func->isDefaulted());
+
+  // TODO(b/230693710): Add complete support for defaulted functions.
+
+  if (const auto* ctor = clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+    if (ctor->isDefaultConstructor()) {
+      const clang::CXXRecordDecl* record = ctor->getParent();
+      if (record->getNumBases() == 0 && !HasRecordTypeFields(record)) {
+        return llvm::DenseSet<const clang::FunctionDecl*>();
+      }
+    }
+  }
+
+  return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                 "unsupported type of defaulted function");
+}
+
+llvm::Expected<llvm::DenseSet<const clang::FunctionDecl*>> GetCallees(
+    const clang::FunctionDecl* func) {
+  using clang::ast_matchers::anyOf;
+  using clang::ast_matchers::cxxConstructExpr;
+  using clang::ast_matchers::declRefExpr;
+  using clang::ast_matchers::expr;
+  using clang::ast_matchers::findAll;
+  using clang::ast_matchers::functionDecl;
+  using clang::ast_matchers::hasDeclaration;
+  using clang::ast_matchers::match;
+  using clang::ast_matchers::memberExpr;
+  using clang::ast_matchers::to;
+
+  func = func->getDefinition();
+
+  if (!func) return llvm::DenseSet<const clang::FunctionDecl*>();
+
+  const clang::Stmt* body = func->getBody();
+  if (!body) {
+    // TODO(b/230693710): Do this unconditionally for defaulted functions, even
+    // if they happen to have a body (because something caused Sema to create a
+    // body for them). We can't do this yet because we don't have full support
+    // for defaulted functions yet, so we would break tests where we happen to
+    // have a body for the defaulted function today.
+    if (func->isDefaulted()) {
+      return GetDefaultedFunctionCallees(func);
+    }
+
+    return llvm::createStringError(llvm::inconvertibleErrorCode(),
+                                   "Declaration-only!");
+  }
+
+  llvm::SmallVector<const clang::Stmt*> body_parts;
+
+  body_parts.push_back(body);
+
+  if (const auto* constructor =
+          clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+    for (const auto* init : constructor->inits()) {
+      body_parts.push_back(init->getInit());
+    }
+  }
+
+  llvm::DenseSet<const clang::FunctionDecl*> callees;
+  for (const auto& body_part : body_parts) {
+    for (const auto& node : match(
+             findAll(expr(anyOf(
+                 declRefExpr(to(functionDecl().bind("function"))),
+                 memberExpr(hasDeclaration(functionDecl().bind("function")))))),
+             *body_part, func->getASTContext())) {
+      const auto* fn = node.getNodeAs<clang::FunctionDecl>("function");
+      callees.insert(fn->getCanonicalDecl());
+    }
+    for (const auto& node :
+         match(findAll(cxxConstructExpr().bind("cxx_construct")), *body_part,
+               func->getASTContext())) {
+      const auto* ctor_exp =
+          node.getNodeAs<clang::CXXConstructExpr>("cxx_construct");
+      if (auto ctor = ctor_exp->getConstructor()) {
+        callees.insert(ctor);
+      }
+    }
+  }
+
+  return std::move(callees);
+}
+
+// Looks for `func` in the `visited_call_stack`. If found it marks `func` and
+// each function that came after it as being part of the cycle. This marking is
+// stored in the `VisitedCallStackEntry`.
+bool FindAndMarkCycleWithFunc(
+    llvm::SmallVectorImpl<VisitedCallStackEntry>& visited_call_stack,
+    const clang::FunctionDecl* func) {
+  // We look for recursive cycles in a simple (but potentially slow for huge
+  // call graphs) way. If we reach a function that is already on the call stack
+  // (i.e. in `visited`), we declare `func`, and every other function after
+  // where `func` was seen in `visited` as being part of a cycle. Then a cycle
+  // graph is a contiguous set of functions in the `visited` call stack that are
+  // marked as being in a cycle.
+  bool found_cycle = false;
+  for (size_t i = visited_call_stack.size(); i > 0; --i) {
+    const auto& stack_entry = visited_call_stack[i - 1];
+    if (stack_entry.func == func) {
+      found_cycle = true;
+      for (; i <= visited_call_stack.size(); ++i) {
+        auto& mut_stack_entry = visited_call_stack[i - 1];
+        mut_stack_entry.in_cycle = true;
+      }
+      break;
+    }
+  }
+  return found_cycle;
+}
+
+llvm::SmallVector<const clang::FunctionDecl*> GetAllFunctionDefinitions(
+    const clang::TranslationUnitDecl* tu) {
+  using clang::ast_matchers::findAll;
+  using clang::ast_matchers::functionDecl;
+  using clang::ast_matchers::hasBody;
+  using clang::ast_matchers::isDefinition;
+  using clang::ast_matchers::match;
+  using clang::ast_matchers::stmt;
+
+  llvm::SmallVector<const clang::FunctionDecl*> functions;
+
+  // For now we specify 'hasBody' to skip functions that don't have a body and
+  // are not called. TODO(veluca): a function might be used in other ways.
+  for (const auto& node : match(
+           findAll(functionDecl(isDefinition(), hasBody(stmt())).bind("func")),
+           tu->getASTContext())) {
+    const auto* func = node.getNodeAs<clang::FunctionDecl>("func");
+    assert(func);
+    functions.push_back(func);
+  }
+
+  return functions;
+}
+
+BaseToOverrides BuildBaseToOverrides(const clang::TranslationUnitDecl* tu) {
+  BaseToOverrides base_to_overrides;
+  for (const clang::FunctionDecl* f : GetAllFunctionDefinitions(tu)) {
+    auto* func = clang::dyn_cast<clang::CXXMethodDecl>(f);
+    if (!func) continue;
+    func = func->getCanonicalDecl();
+    if (!func->isVirtual()) continue;
+    for (const auto* base : func->overridden_methods()) {
+      base_to_overrides[base->getCanonicalDecl()].insert(func);
+    }
+  }
+  return base_to_overrides;
+}
+
+void GetBaseMethods(const clang::CXXMethodDecl* cxxmethod,
+                    llvm::DenseSet<const clang::CXXMethodDecl*>& bases) {
+  if (cxxmethod->size_overridden_methods() == 0) {
+    // TODO(kinuko): It is not fully clear if one method may ever have multiple
+    // base methods. If not this can simply return a single CXXMethodDecl rathr
+    // than a set.
+    bases.insert(cxxmethod);
+    return;
+  }
+  for (const auto* base : cxxmethod->overridden_methods()) {
+    // Each method's overridden_methods() only returns an immediate base but not
+    // ancestors of further than that, so recursively call it.
+    GetBaseMethods(base, bases);
+  }
+}
+
+std::optional<FunctionLifetimes> GetFunctionLifetimesFromAnalyzed(
+    const clang::FunctionDecl* canonical_func,
+    const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        analyzed) {
+  auto found = analyzed.find(canonical_func);
+  if (found == analyzed.end()) return std::nullopt;
+  auto* lifetimes = std::get_if<FunctionLifetimes>(&found->second);
+  if (!lifetimes) return std::nullopt;
+  return *lifetimes;
+}
+
+// Update the function lifetimes of `func` with its immediate `overrides` so
+// that the lifetimes of the base method will become least permissive. The
+// updates will be reflected from the base to its final overrides as this is
+// recursively called.
+void UpdateFunctionLifetimesWithOverrides(
+    const clang::FunctionDecl* func,
+    llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        analyzed,
+    const llvm::SmallPtrSet<const clang::CXXMethodDecl*, 2>& overrides) {
+  const auto* canonical = func->getCanonicalDecl();
+  const auto* method = clang::dyn_cast<clang::CXXMethodDecl>(func);
+  assert(method != nullptr);
+  assert(method->isVirtual());
+  static_cast<void>(method);
+
+  auto opt_lifetimes = GetFunctionLifetimesFromAnalyzed(canonical, analyzed);
+  if (!opt_lifetimes) return;
+  FunctionLifetimes base_lifetimes = *opt_lifetimes;
+
+  assert(base_lifetimes.IsValidForDecl(func));
+
+  for (const auto* overriding : overrides) {
+    if (overriding->getNumParams() != func->getNumParams()) {
+      llvm::errs() << "Param number mismatches between "
+                   << method->getParent()->getNameAsString() << " and "
+                   << overriding->getParent()->getNameAsString() << "\n";
+      func->dump();
+      overriding->dump();
+      assert(false);
+      return;
+    }
+    auto opt_override_lifetimes = GetFunctionLifetimesFromAnalyzed(
+        overriding->getCanonicalDecl(), analyzed);
+    if (!opt_override_lifetimes) continue;
+    FunctionLifetimes override_lifetimes = *opt_override_lifetimes;
+
+    base_lifetimes =
+        ConstrainLifetimes(base_lifetimes,
+                           override_lifetimes.ForOverriddenMethod(method))
+            .first;
+  }
+  analyzed[canonical] = base_lifetimes;
+}
+
+llvm::Error AnalyzeRecursiveFunctions(
+    llvm::ArrayRef<VisitedCallStackEntry> funcs,
+    llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        analyzed,
+    const DiagnosticReporter& diag_reporter, FunctionDebugInfoMap* debug_info) {
+  for (const auto [func, in_cycle, _] : funcs) {
+    assert(in_cycle);
+
+    // Grab the initial FunctionLifetimes for each function in the cycle,
+    // without doing a dataflow analysis, which would need other functions
+    // in the cycle to already be analyzed.
+    auto func_lifetimes_result = ConstructFunctionLifetimes(
+        func,
+        FunctionAnalysis{
+            .object_repository = ObjectRepository(func),
+        },
+        diag_reporter);
+    if (!func_lifetimes_result) {
+      return func_lifetimes_result.takeError();
+    }
+    analyzed[func->getCanonicalDecl()] = func_lifetimes_result.get();
+  }
+
+  int64_t expected_iterations = 0;
+  for (const auto [func, _1, _2] : funcs) {
+    expected_iterations =
+        std::max(expected_iterations, int64_t{func->getNumParams()});
+  }
+  // Add 1 for the last iteration that sees nothing changed.
+  expected_iterations += 1;
+
+  // Analyze all lifetimes in the cycle repeatedly with dataflow analysis
+  // until they stabilize.
+  bool func_lifetimes_changed = true;
+  for (int64_t count = 0; func_lifetimes_changed; ++count) {
+    func_lifetimes_changed = false;
+
+    if (count > expected_iterations) {
+      return llvm::createStringError(
+          llvm::inconvertibleErrorCode(),
+          absl::StrFormat("Recursive cycle requires more than the expected "
+                          "%u iterations to resolve!",
+                          expected_iterations));
+    }
+
+    for (const auto [func, in_cycle, _] : funcs) {
+      auto analysis_result =
+          AnalyzeSingleFunctionBody(func, analyzed, diag_reporter, debug_info);
+      if (!analysis_result) {
+        return analysis_result.takeError();
+      }
+      auto func_lifetimes_result = ConstructFunctionLifetimes(
+          func, std::move(analysis_result.get()), diag_reporter);
+      if (!func_lifetimes_result) {
+        return func_lifetimes_result.takeError();
+      }
+      // TODO(danakj): We can avoid this structural comparison and just do a
+      // check for equality if AnalyzeSingleFunction would reuse Lifetimes
+      // from the existing FunctionLifetime for its parameters/return/this.
+      // Currently it makes a new set of Lifetimes each time we do the analyze
+      // step, but the actual Lifetime ids aren't meaningful, only where and
+      // how often a given Lifetime repeats is meaningful.
+      FunctionLifetimesOrError& existing_result =
+          analyzed[func->getCanonicalDecl()];
+      if (std::holds_alternative<FunctionLifetimes>(existing_result) &&
+          !IsIsomorphic(std::get<FunctionLifetimes>(existing_result),
+                        func_lifetimes_result.get())) {
+        existing_result = func_lifetimes_result.get();
+        func_lifetimes_changed = true;
+      }
+    }
+  }
+
+  return llvm::Error::success();
+}
+
+// The entry point for analyzing a function named by `func`.
+//
+// This function is recursive as it searches for and walks through all CallExpr
+// instances, calling this function again for each function. This is done to
+// analyze the leaves of the call graph first, so that when analyzing a given
+// function, all the functions it calls have already been analyzed.
+//
+// This function also handles walking through recursive cycles of function
+// calls. When a cycle is detected, we:
+// 1. Do not analyze any of the functions until the cycle is fully explored and
+//    we've returned to the entry point to the cycle.
+// 2. At that point, we generate a FunctionLifetimes for each function in the
+//    cycle, where the lifetimes are all completely disconnected.
+// 3. Then we analyze each function in the cycle based on those
+//    FunctionLifetimes, connecting lifetimes within the body of each function.
+//    This changes a given function's resulting FunctionLifetimes, which can
+//    affect the callers to it.
+// 4. Thus we repeat step 3 until we see that the FunctionLifetimes have stopped
+//    changing when we analyze each function in the cycle.
+void AnalyzeFunctionRecursive(
+    llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        analyzed,
+    llvm::SmallVectorImpl<VisitedCallStackEntry>& visited,
+    const clang::FunctionDecl* func,
+    const LifetimeAnnotationContext& lifetime_context,
+    const DiagnosticReporter& diag_reporter, FunctionDebugInfoMap* debug_info,
+    const BaseToOverrides& base_to_overrides) {
+  // Make sure we're always using the canonical declaration when using the
+  // function as a key in maps and sets.
+  func = func->getCanonicalDecl();
+
+  // See if we have finished analyzing the function.
+  bool is_analyzed = analyzed.count(func) > 0;
+
+  auto* cxxmethod = clang::dyn_cast<clang::CXXMethodDecl>(func);
+  bool is_virtual = cxxmethod != nullptr && cxxmethod->isVirtual();
+  bool is_pure_virtual = is_virtual && cxxmethod->isPure();
+
+  if (func->getBuiltinID() != 0) {
+    return;
+  }
+
+  if (!func->isDefined() && !is_pure_virtual && !is_analyzed) {
+    FunctionLifetimes annotations;
+    if (llvm::Error err = GetLifetimeAnnotations(func, lifetime_context)
+                              .moveInto(annotations)) {
+      analyzed[func] = FunctionAnalysisError(err);
+    } else {
+      analyzed[func] = annotations;
+    }
+    return;
+  }
+
+  // Check if we're in an overrides traversal for a virtual method.
+  bool in_overrides_traversal =
+      visited.empty() ? false : visited.back().in_overrides_traversal;
+
+  if (is_analyzed && !in_overrides_traversal) {
+    // This function is already analyzed and this analysis is not for an
+    // overrides traversal (where repeated update may happen).
+    // TODO(kinuko): Avoid repeatedly visit the same virtual methods again and
+    // again if all the methods in the same overriding chain are already
+    // analyzed.
+    return;
+  }
+
+  if (!in_overrides_traversal && FindAndMarkCycleWithFunc(visited, func)) {
+    // Defer analyzing the cycle until we have fully explored the recursive
+    // cycle graph.
+    // This cycle check should exclude in_overrides_traversal case, because the
+    // traversal can come back to the same function while traversing from its
+    // overridden base method, e.g. when we see Child::f() we start the analysis
+    // from its overridden implementation Base::f() and then recursively look
+    // into its overrides until it reaches its final overrides (and it should
+    // see Child::f() on its way.
+
+    // TODO(kinuko): We may return here when Base::f() calls f() even when
+    // it has overrides, and if it happens AnalyzeRecursiveFunctions don't
+    // look into the overrides so the Base::f() lifetime is not updated.
+    // See DISABLED_FunctionVirtualInheritanceWithComplexRecursion tests.
+    return;
+  }
+
+  auto maybe_callees = GetCallees(func);
+  if (!maybe_callees) {
+    analyzed[func] = FunctionAnalysisError(maybe_callees.takeError());
+    return;
+  }
+
+  // Keep track of where `func` is found in the call stack. It may not be at the
+  // top anymore after we return from calling `AnalyzeFunctionRecursive()` if
+  // `func` is part of a recursive cycle, as we keep all members of the
+  // recursive cycle in the `visited` stack until we explore the whole graph and
+  // then analyze it all.
+  size_t func_in_visited = visited.size();
+  visited.emplace_back(VisitedCallStackEntry{
+      .func = func, .in_cycle = false, .in_overrides_traversal = false});
+
+  for (auto& callee : maybe_callees.get()) {
+    if (analyzed.count(callee)) {
+      continue;
+    }
+    AnalyzeFunctionRecursive(analyzed, visited, callee, lifetime_context,
+                             diag_reporter, debug_info, base_to_overrides);
+  }
+
+  llvm::DenseSet<const clang::CXXMethodDecl*> bases;
+  llvm::SmallPtrSet<const clang::CXXMethodDecl*, 2> overrides;
+
+  // This is a virtual method and we want to recursively analyze the inheritance
+  // chain and update the base methods with their overrides. The base methods
+  // may be visited and updated repeatedly.
+  if (is_virtual) {
+    assert(cxxmethod != nullptr);
+    visited[func_in_visited].in_overrides_traversal = true;
+    if (!in_overrides_traversal) {
+      // If it's a virtual method and we are not yet in an overrides traversal,
+      // start from the base method.
+      GetBaseMethods(cxxmethod, bases);
+      for (const auto* base : bases) {
+        AnalyzeFunctionRecursive(analyzed, visited, base, lifetime_context,
+                                 diag_reporter, debug_info, base_to_overrides);
+      }
+    } else {
+      // We are in an overrides traversal for a virtual method starting from its
+      // base method. Recursively look into the overrides that this TU knows
+      // about, so that the base method's analysis result can be updated with
+      // the overrides (that are discovered in this TU).
+      auto iter = base_to_overrides.find(cxxmethod->getCanonicalDecl());
+      if (iter != base_to_overrides.end()) {
+        overrides = iter->second;
+        for (const auto* derived : overrides) {
+          AnalyzeFunctionRecursive(analyzed, visited, derived, lifetime_context,
+                                   diag_reporter, debug_info,
+                                   base_to_overrides);
+        }
+      }
+    }
+    visited[func_in_visited].in_overrides_traversal = false;
+  }
+
+  // Recursing through CallExprs should not remove `func` from the stack, though
+  // there may be more on the stack after `func` if they are all part of a
+  // recursive cycle graph.
+  assert(visited[func_in_visited].func == func);
+  if (func_in_visited < visited.size() - 1) {
+    for (size_t i = func_in_visited; i < visited.size(); ++i) {
+      assert(visited[i].in_cycle);
+    }
+  }
+
+  // Once we return back here, there are 3 possibilities for `func`.
+  //
+  // 1. If `func` is part of a cycle, but was not the first entry point of the
+  //    cycle, then we defer analyzing `func` until we get back to the entry
+  //    point. We look for this by seeing if there is another function marked as
+  //    being in a cycle above `func` in the `visited` call stack. Note that we
+  //    will leave `func` in the `visited` call stack when we return so that
+  //    once we get back to the recursive cycle's entry point, we can see all
+  //    the functions that are part of the cycle graph.
+  // 2. If `func` was not part of a cycle, we can analyze it and expect it to
+  //    have valid FunctionLifetimes already generated for anything it calls.
+  // 3. Otherwise, we collect the whole cycle (which may be just the `func` if
+  //    it calls itself directly), and we analyze the cycle as a whole.
+
+  if (func_in_visited > 0 && visited[func_in_visited].in_cycle &&
+      visited[func_in_visited - 1].in_cycle) {
+    // Case 1. In a recursive cycle, but not the entry point.
+    return;
+  }
+  if (!visited[func_in_visited].in_cycle) {
+    // Case 2. Not part of a cycle.
+    if (bases.empty()) {
+      // This function is not where we initiated an overrides traversal from its
+      // base methods.
+      auto analysis_result =
+          AnalyzeSingleFunctionBody(func, analyzed, diag_reporter, debug_info);
+      if (!analysis_result) {
+        analyzed[func] = FunctionAnalysisError(analysis_result.takeError());
+      } else {
+        auto func_lifetimes_result = ConstructFunctionLifetimes(
+            func, std::move(analysis_result.get()), diag_reporter);
+        if (!func_lifetimes_result) {
+          analyzed[func] =
+              FunctionAnalysisError(func_lifetimes_result.takeError());
+        } else {
+          analyzed[func] = func_lifetimes_result.get();
+        }
+      }
+    } else {
+      // In this branch we have initiated (and finished) an overrides
+      // traversal starting with its base method, and the traversal for this
+      // function must be already done as a part of the overrides traversal.
+      assert(is_virtual);
+      assert(analyzed.count(func) > 0);
+    }
+  } else {
+    // Case 3. The entry point to a recursive cycle.
+    auto funcs_in_cycle =
+        llvm::ArrayRef<VisitedCallStackEntry>(visited).drop_front(
+            func_in_visited);
+    if (llvm::Error err = AnalyzeRecursiveFunctions(
+            funcs_in_cycle, analyzed, diag_reporter, debug_info)) {
+      for (const auto [func_in_cycle, _1, _2] : funcs_in_cycle) {
+        analyzed[func_in_cycle] = FunctionAnalysisError(err);
+      }
+    }
+  }
+
+  // If this has overrides and we're in an overrides traversal, the lifetimes
+  // need to be (recursively) updated with the results of the overrides.
+  if (in_overrides_traversal) {
+    UpdateFunctionLifetimesWithOverrides(func, analyzed, overrides);
+  }
+
+  // Once we have finished analyzing `func`, we can remove it from the visited
+  // stack, along with anything it called in a recursive cycle (which will be
+  // found after `func` in the `visited` call stack.
+  visited.resize(func_in_visited);
+}
+
+llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+AnalyzeTranslationUnitAndCollectTemplates(
+    const clang::TranslationUnitDecl* tu,
+    const LifetimeAnnotationContext& lifetime_context,
+    const DiagnosticReporter& diag_reporter, FunctionDebugInfoMap* debug_info,
+    llvm::DenseMap<clang::FunctionTemplateDecl*, const clang::FunctionDecl*>&
+        uninstantiated_templates,
+    const BaseToOverrides& base_to_overrides) {
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError> result;
+  llvm::SmallVector<VisitedCallStackEntry> visited;
+
+  for (const clang::FunctionDecl* func : GetAllFunctionDefinitions(tu)) {
+    // Skip templated functions.
+    if (func->isTemplated()) {
+      clang::FunctionTemplateDecl* template_decl =
+          func->getDescribedFunctionTemplate();
+      if (template_decl) {
+        uninstantiated_templates.insert({template_decl, func});
+      }
+      continue;
+    }
+
+    if (func->isFunctionTemplateSpecialization()) {
+      auto* info = func->getTemplateSpecializationInfo();
+      uninstantiated_templates.erase(info->getTemplate());
+    }
+
+    // For some reason that's not clear to mboehme@, the AST matcher is
+    // returning two matches for every function definition; maybe there are two
+    // different paths from a TranslationUnitDecl to a function definition.
+    // This doesn't really have any ill effect, however, as
+    // AnalyzeFunctionRecursive() bails out anyway if it has analyzed the
+    // function before.
+
+    AnalyzeFunctionRecursive(result, visited, func, lifetime_context,
+                             diag_reporter, debug_info, base_to_overrides);
+  }
+
+  return result;
+}
+
+std::string GetFunctionUSRString(const clang::Decl* func) {
+  llvm::SmallString</*inline size=*/128> usr;
+  if (clang::index::generateUSRForDecl(func, usr)) {
+    llvm::errs() << "Could not generate USR for ";
+    func->dump();
+    assert(false);
+    return std::string();
+  }
+  return std::string(usr.data(), usr.size());
+}
+
+// Run AnalyzeFunctionRecursive with `context`. Report results through
+// `result_callback` and update `debug_info` using USR strings to map functions
+// to the original ASTContext.
+void AnalyzeTemplateFunctionsInSeparateASTContext(
+    const LifetimeAnnotationContext& lifetime_context,
+    const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+        initial_result,
+    const FunctionAnalysisResultCallback& result_callback,
+    const DiagnosticReporter& diag_reporter, FunctionDebugInfoMap* debug_info,
+    const std::map<std::string, const clang::FunctionDecl*>&
+        template_usr_to_decl,
+    const BaseToOverrides& base_to_overrides, clang::ASTContext& context) {
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+      inner_result;
+  llvm::SmallVector<VisitedCallStackEntry> inner_visited;
+  FunctionDebugInfoMap inner_debug_info;
+
+  for (const clang::FunctionDecl* func :
+       GetAllFunctionDefinitions(context.getTranslationUnitDecl())) {
+    // Skip templated functions.
+    if (func->isTemplated()) continue;
+
+    AnalyzeFunctionRecursive(inner_result, inner_visited, func,
+                             lifetime_context, diag_reporter, &inner_debug_info,
+                             base_to_overrides);
+  }
+
+  // We need to remap the results with FunctionDecl* in the
+  // original ASTContext. (Because this context goes away after
+  // this)
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+      merged_result = initial_result;
+  for (const auto& [decl, lifetimes_or_error] : inner_result) {
+    if (!decl->isFunctionTemplateSpecialization()) continue;
+    auto* tmpl = decl->getTemplateSpecializationInfo()->getTemplate();
+    auto iter = template_usr_to_decl.find(GetFunctionUSRString(tmpl));
+    if (iter != template_usr_to_decl.end()) {
+      merged_result.insert({iter->second, lifetimes_or_error});
+    }
+  }
+  for (const auto& [decl, lifetimes_or_error] : merged_result) {
+    result_callback(decl, lifetimes_or_error);
+  }
+  for (auto& [decl, info] : inner_debug_info) {
+    if (!decl->isFunctionTemplateSpecialization()) continue;
+    auto* tmpl = decl->getTemplateSpecializationInfo()->getTemplate();
+    auto iter = template_usr_to_decl.find(GetFunctionUSRString(tmpl));
+    if (iter != template_usr_to_decl.end()) (*debug_info)[iter->second] = info;
+  }
+}
+
+DiagnosticReporter DiagReporterForDiagEngine(
+    clang::DiagnosticsEngine& diag_engine) {
+  return
+      [&diag_engine](clang::SourceLocation location, clang::StringRef message,
+                     clang::DiagnosticIDs::Level level) {
+        return diag_engine.Report(
+            location,
+            diag_engine.getDiagnosticIDs()->getCustomDiagID(level, message));
+      };
+}
+
+}  // namespace
+
+bool IsIsomorphic(const FunctionLifetimes& a, const FunctionLifetimes& b) {
+  return !ConstrainLifetimes(a, b).second && !ConstrainLifetimes(b, a).second;
+}
+
+FunctionLifetimesOrError AnalyzeFunction(
+    const clang::FunctionDecl* func,
+    const LifetimeAnnotationContext& lifetime_context,
+    FunctionDebugInfo* debug_info) {
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError> analyzed;
+  llvm::SmallVector<VisitedCallStackEntry> visited;
+  std::optional<FunctionDebugInfoMap> debug_info_map;
+  if (debug_info) {
+    debug_info_map.emplace();
+  }
+  DiagnosticReporter diag_reporter =
+      DiagReporterForDiagEngine(func->getASTContext().getDiagnostics());
+  AnalyzeFunctionRecursive(
+      analyzed, visited, func, lifetime_context, diag_reporter,
+      debug_info_map ? &debug_info_map.value() : nullptr, BaseToOverrides());
+  if (debug_info) {
+    *debug_info = debug_info_map->lookup(func);
+  }
+  return analyzed.lookup(func);
+}
+
+llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+AnalyzeTranslationUnit(const clang::TranslationUnitDecl* tu,
+                       const LifetimeAnnotationContext& lifetime_context,
+                       DiagnosticReporter diag_reporter,
+                       FunctionDebugInfoMap* debug_info) {
+  if (!diag_reporter) {
+    diag_reporter =
+        DiagReporterForDiagEngine(tu->getASTContext().getDiagnostics());
+  }
+
+  llvm::DenseMap<clang::FunctionTemplateDecl*, const clang::FunctionDecl*>
+      uninstantiated_templates;
+
+  // Builds a map from a base method to its overrides within this TU. It will
+  // not find out all the overrides, but still cover (and can partially update)
+  // all the base methods that this TU implements.
+  auto base_to_overrides = BuildBaseToOverrides(tu);
+
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError> result =
+      AnalyzeTranslationUnitAndCollectTemplates(
+          tu, lifetime_context, diag_reporter, debug_info,
+          uninstantiated_templates, base_to_overrides);
+
+  return result;
+}
+
+void AnalyzeTranslationUnitWithTemplatePlaceholder(
+    const clang::TranslationUnitDecl* tu,
+    const LifetimeAnnotationContext& lifetime_context,
+    const FunctionAnalysisResultCallback& result_callback,
+    DiagnosticReporter diag_reporter, FunctionDebugInfoMap* debug_info) {
+  if (!diag_reporter) {
+    diag_reporter =
+        DiagReporterForDiagEngine(tu->getASTContext().getDiagnostics());
+  }
+
+  llvm::DenseMap<clang::FunctionTemplateDecl*, const clang::FunctionDecl*>
+      uninstantiated_templates;
+
+  // Builds a map from a base method to its overrides within this TU. It will
+  // not find out all the overrides, but still cover (and can partially update)
+  // all the base methods that this TU implements.
+  auto base_to_overrides = BuildBaseToOverrides(tu);
+
+  llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+      initial_result = AnalyzeTranslationUnitAndCollectTemplates(
+          tu, lifetime_context, diag_reporter, debug_info,
+          uninstantiated_templates, base_to_overrides);
+
+  // Make a map from USRString to funcDecls in the original ASTContext.
+  std::map<std::string, const clang::FunctionDecl*> template_usr_to_decl;
+  for (const auto& [tmpl, func] : uninstantiated_templates) {
+    template_usr_to_decl[GetFunctionUSRString(tmpl)] = func;
+  }
+
+  GeneratedCode code_with_placeholder;
+  if (llvm::Error err =
+          GenerateTemplateInstantiationCode(tu, uninstantiated_templates)
+              .moveInto(code_with_placeholder)) {
+    FunctionAnalysisError analysis_error(err);
+    for (const auto& [tmpl, func] : uninstantiated_templates) {
+      result_callback(func, analysis_error);
+    }
+    return;
+  }
+
+  // A callback to call AnalyzeFunctionRecursive again with template
+  // placeholders. This is passed to RunToolOnCodeWithOverlay below.
+  auto analyze_with_placeholder =
+      [&lifetime_context, &initial_result, &result_callback, &diag_reporter,
+       &debug_info, &template_usr_to_decl,
+       &base_to_overrides](clang::ASTContext& context) {
+        AnalyzeTemplateFunctionsInSeparateASTContext(
+            lifetime_context, initial_result, result_callback, diag_reporter,
+            debug_info, template_usr_to_decl, base_to_overrides, context);
+      };
+
+  // Run `analyze_with_placeholder` in a separate ASTContext on top of an
+  // overlaid filesystem with the `code_with_placeholder` file.
+  RunToolOnCodeWithOverlay(tu->getASTContext(), code_with_placeholder.filename,
+                           code_with_placeholder.code,
+                           analyze_with_placeholder);
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/analyze.h b/lifetime_analysis/analyze.h
new file mode 100644
index 0000000..2dc85a1
--- /dev/null
+++ b/lifetime_analysis/analyze.h
@@ -0,0 +1,86 @@
+// 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_ANALYZE_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_ANALYZE_H_
+
+#include <functional>
+#include <string>
+#include <variant>
+
+#include "lifetime_analysis/lifetime_analysis.h"
+#include "lifetime_annotations/function_lifetimes.h"
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "clang/AST/Decl.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallVector.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// Lifetime analysis debug info for a single function.
+struct FunctionDebugInfo {
+  // Human-readable representation of the function's AST.
+  std::string ast;
+
+  // Human-readable representation of the function's ObjectRepository.
+  std::string object_repository;
+
+  // A graph of the exit-block's points-to map in .dot file format.
+  std::string points_to_map_dot;
+
+  // A graph of the CFG in .dot file format.
+  std::string cfg_dot;
+};
+
+// Returns if the two FunctionLifetimes have the same structures, without
+// requiring them to have the same exact Lifetimes. They have the same
+// structure if unique vs reoccuring Lifetimes in `a` and `b` are found
+// in the same positions.
+bool IsIsomorphic(const FunctionLifetimes& a, const FunctionLifetimes& b);
+
+// A map from an analyzed function to the corresponding debug info.
+using FunctionDebugInfoMap =
+    llvm::DenseMap<const clang::FunctionDecl*, FunctionDebugInfo>;
+
+// Runs a static analysis on `func` and returns the result.
+FunctionLifetimesOrError AnalyzeFunction(
+    const clang::FunctionDecl* func,
+    const LifetimeAnnotationContext& lifetime_context,
+    FunctionDebugInfo* debug_info = nullptr);
+
+// Runs a static analysis on all function definitions in `tu`.
+// The map that is returned references functions by their canonical declaration.
+llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>
+AnalyzeTranslationUnit(const clang::TranslationUnitDecl* tu,
+                       const LifetimeAnnotationContext& lifetime_context,
+                       DiagnosticReporter diag_reporter = {},
+                       FunctionDebugInfoMap* debug_info = nullptr);
+
+// Callback that is used to report function analysis results.
+// Do not retain the `FunctionDecl*`, the `FunctionLifetimes`, or other objects
+// reachable from them for later use; they refer to entities from an
+// `ASTContext` that may be destroyed as soon as the callback returns. In
+// particular, note that this also applies to `clang::Type`s contained in the
+// `FunctionLifetimes`.
+using FunctionAnalysisResultCallback =
+    std::function<void(const clang::FunctionDecl* func,
+                       const FunctionLifetimesOrError& lifetimes_or_error)>;
+
+// Runs a static analysis on all function definitions in `tu`.
+// Analyzes and reports results for uninstantiated templates by instantiating
+// them with placeholder types, reporting results via `result_callback`.
+void AnalyzeTranslationUnitWithTemplatePlaceholder(
+    const clang::TranslationUnitDecl* tu,
+    const LifetimeAnnotationContext& lifetime_context,
+    const FunctionAnalysisResultCallback& result_callback,
+    DiagnosticReporter diag_reporter = {},
+    FunctionDebugInfoMap* debug_info = nullptr);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_ANALYZE_H_
diff --git a/lifetime_analysis/builtin_lifetimes.cc b/lifetime_analysis/builtin_lifetimes.cc
new file mode 100644
index 0000000..bd442b2
--- /dev/null
+++ b/lifetime_analysis/builtin_lifetimes.cc
@@ -0,0 +1,96 @@
+// 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/builtin_lifetimes.h"
+
+#include <optional>
+#include <string>
+
+#include "absl/strings/str_cat.h"
+#include "lifetime_annotations/function_lifetimes.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Type.h"
+#include "clang/Basic/Builtins.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+namespace {
+
+class ForwardAndMoveFactory : public FunctionLifetimeFactory {
+  llvm::Expected<ValueLifetimes> CreateParamLifetimes(
+      clang::QualType type) const override {
+    return ValueLifetimes::Create(type, [](clang::QualType, llvm::StringRef) {
+      return Lifetime::CreateVariable();
+    });
+  }
+
+  llvm::Expected<ValueLifetimes> CreateReturnLifetimes(
+      clang::QualType type,
+      const llvm::SmallVector<ValueLifetimes>& param_lifetimes,
+      const std::optional<ValueLifetimes>& /*this_lifetimes*/) const override {
+    assert(param_lifetimes.size() == 1);
+    // `forward` and `move` convert from one type of reference to the other; the
+    // lifetimes in the pointees of these references are the same.
+    return ValueLifetimes::ForPointerLikeType(
+        type, param_lifetimes[0].GetPointeeLifetimes());
+  }
+};
+
+}  // namespace
+
+FunctionLifetimesOrError GetBuiltinLifetimes(const clang::FunctionDecl* decl) {
+  unsigned builtin_id = decl->getBuiltinID();
+  const auto& builtin_info = decl->getASTContext().BuiltinInfo;
+  assert(builtin_id != 0);
+
+  if (!builtin_info.hasPtrArgsOrResult(builtin_id) &&
+      !builtin_info.hasReferenceArgsOrResult(builtin_id)) {
+    return FunctionLifetimes::CreateForDecl(
+               decl, FunctionLifetimeFactorySingleCallback(
+                         [](clang::QualType, llvm::StringRef) {
+                           assert(false);
+                           return Lifetime();
+                         }))
+        .get();
+  }
+  switch (builtin_id) {
+    case clang::Builtin::BI__builtin_addressof:
+      return ParseLifetimeAnnotations(decl, "a -> a").get();
+    case clang::Builtin::BIstrtod:
+    case clang::Builtin::BIstrtof:
+      return ParseLifetimeAnnotations(decl, "a, (a, b)").get();
+    case clang::Builtin::BIstrtoll:
+    case clang::Builtin::BIstrtol:
+      return ParseLifetimeAnnotations(decl, "a, (a, b), ()").get();
+    case clang::Builtin::BI__builtin_memchr:
+      return ParseLifetimeAnnotations(decl, "a, (), () -> a").get();
+    case clang::Builtin::BI__builtin_strchr:
+    case clang::Builtin::BI__builtin_strrchr:
+      return ParseLifetimeAnnotations(decl, "a, () -> a").get();
+    case clang::Builtin::BI__builtin_strstr:
+    case clang::Builtin::BI__builtin_strpbrk:
+      return ParseLifetimeAnnotations(decl, "a, b -> a").get();
+    case clang::Builtin::BIforward:
+    case clang::Builtin::BImove: {
+      FunctionLifetimes result;
+      return FunctionLifetimes::CreateForDecl(decl, ForwardAndMoveFactory())
+          .get();
+    }
+    // TODO(veluca): figure out variadic functions.
+    default:
+      return FunctionAnalysisError(absl::StrCat(
+          "Unknown builtin: '", builtin_info.getName(builtin_id), "'"));
+  }
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/builtin_lifetimes.h b/lifetime_analysis/builtin_lifetimes.h
new file mode 100644
index 0000000..cce2a3b
--- /dev/null
+++ b/lifetime_analysis/builtin_lifetimes.h
@@ -0,0 +1,24 @@
+// 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_BUILTIN_LIFETIMES_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_BUILTIN_LIFETIMES_H_
+
+#include <functional>
+#include <string>
+#include <variant>
+
+#include "lifetime_annotations/function_lifetimes.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+FunctionLifetimesOrError GetBuiltinLifetimes(const clang::FunctionDecl* decl);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_BUILTIN_LIFETIMES_H_
diff --git a/lifetime_analysis/lifetime_analysis.cc b/lifetime_analysis/lifetime_analysis.cc
new file mode 100644
index 0000000..212f9ce
--- /dev/null
+++ b/lifetime_analysis/lifetime_analysis.cc
@@ -0,0 +1,1087 @@
+// 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/lifetime_analysis.h"
+
+#include <iostream>
+#include <memory>
+#include <optional>
+#include <string>
+#include <utility>
+#include <variant>
+#include <vector>
+
+#include "lifetime_analysis/builtin_lifetimes.h"
+#include "lifetime_analysis/object.h"
+#include "lifetime_analysis/object_repository.h"
+#include "lifetime_analysis/object_set.h"
+#include "lifetime_analysis/pointer_compatibility.h"
+#include "lifetime_analysis/points_to_map.h"
+#include "lifetime_analysis/visit_lifetimes.h"
+#include "lifetime_annotations/function_lifetimes.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/pointee_type.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/OperationKinds.h"
+#include "clang/AST/Stmt.h"
+#include "clang/AST/StmtVisitor.h"
+#include "clang/AST/TemplateBase.h"
+#include "clang/AST/Type.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/Optional.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+namespace {
+
+class TransferStmtVisitor
+    : public clang::StmtVisitor<TransferStmtVisitor,
+                                std::optional<std::string>> {
+ public:
+  TransferStmtVisitor(
+      ObjectRepository& object_repository, PointsToMap& points_to_map,
+      const clang::FunctionDecl* func,
+      const llvm::DenseMap<const clang::FunctionDecl*,
+                           FunctionLifetimesOrError>& callee_lifetimes,
+      const DiagnosticReporter& diag_reporter)
+      : object_repository_(object_repository),
+        points_to_map_(points_to_map),
+        func_(func),
+        callee_lifetimes_(callee_lifetimes),
+        diag_reporter_(diag_reporter) {}
+
+  std::optional<std::string> VisitExpr(const clang::Expr* expr);
+  std::optional<std::string> VisitDeclRefExpr(
+      const clang::DeclRefExpr* decl_ref);
+  std::optional<std::string> VisitStringLiteral(
+      const clang::StringLiteral* strlit);
+  std::optional<std::string> VisitCastExpr(const clang::CastExpr* cast);
+  std::optional<std::string> VisitReturnStmt(
+      const clang::ReturnStmt* return_stmt);
+  std::optional<std::string> VisitDeclStmt(const clang::DeclStmt* decl_stmt);
+  std::optional<std::string> VisitUnaryOperator(const clang::UnaryOperator* op);
+  std::optional<std::string> VisitArraySubscriptExpr(
+      const clang::ArraySubscriptExpr* subscript);
+  std::optional<std::string> VisitBinaryOperator(
+      const clang::BinaryOperator* op);
+  std::optional<std::string> VisitConditionalOperator(
+      const clang::ConditionalOperator* op);
+  std::optional<std::string> VisitInitListExpr(
+      const clang::InitListExpr* init_list);
+  std::optional<std::string> VisitMaterializeTemporaryExpr(
+      const clang::MaterializeTemporaryExpr* temporary_expr);
+  std::optional<std::string> VisitMemberExpr(const clang::MemberExpr* member);
+  std::optional<std::string> VisitCXXThisExpr(
+      const clang::CXXThisExpr* this_expr);
+  std::optional<std::string> VisitCallExpr(const clang::CallExpr* call);
+  std::optional<std::string> VisitCXXConstructExpr(
+      const clang::CXXConstructExpr* construct_expr);
+
+ private:
+  ObjectRepository& object_repository_;
+  PointsToMap& points_to_map_;
+  const clang::FunctionDecl* func_;
+  const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+      callee_lifetimes_;
+  const DiagnosticReporter& diag_reporter_;
+};
+
+}  // namespace
+
+void TransferInitializer(Object dest, clang::QualType type,
+                         const ObjectRepository& object_repository,
+                         const clang::Expr* init_expr,
+                         PointsToMap& points_to_map) {
+  type = type.getCanonicalType();
+  if (type->isArrayType()) {
+    type = type->castAsArrayTypeUnsafe()->getElementType();
+  }
+
+  // Initializer lists are handled one member/field at a time.
+  if (type->isRecordType()) {
+    if (auto init_list_expr = clang::dyn_cast<clang::InitListExpr>(init_expr)) {
+      // We assume that initializers are always the semantic form of
+      // InitListExpr.
+      assert(init_list_expr->isSemanticForm());
+      size_t init = 0;
+      for (auto f : type->getAs<clang::RecordType>()->getDecl()->fields()) {
+        assert(init < init_list_expr->getNumInits());
+        auto field_init = init_list_expr->getInit(init);
+        ++init;
+        TransferInitializer(object_repository.GetFieldObject(dest, f),
+                            f->getType(), object_repository, field_init,
+                            points_to_map);
+      }
+      return;
+    }
+  }
+
+  if (type->isPointerType() || type->isReferenceType() ||
+      type->isStructureOrClassType()) {
+    ObjectSet init_points_to = points_to_map.GetExprObjectSet(init_expr);
+    // It's important to use "Extend" (not "Set") here because we process
+    // initializers for member variables only _after_ the dataflow analysis has
+    // run.
+    points_to_map.ExtendPointerPointsToSet(dest, init_points_to);
+  }
+}
+
+LifetimeLattice LifetimeAnalysis::initialElement() {
+  return LifetimeLattice(object_repository_.InitialPointsToMap());
+}
+
+std::string LifetimeAnalysis::ToString(const LifetimeLattice& state) {
+  return state.ToString();
+}
+
+bool LifetimeAnalysis::IsEqual(const LifetimeLattice& state1,
+                               const LifetimeLattice& state2) {
+  return state1 == state2;
+}
+
+void LifetimeAnalysis::transfer(const clang::Stmt* stmt, LifetimeLattice& state,
+                                clang::dataflow::Environment& /*environment*/) {
+  if (state.IsError()) return;
+
+  TransferStmtVisitor visitor(object_repository_, state.PointsTo(), func_,
+                              callee_lifetimes_, diag_reporter_);
+  if (std::optional<std::string> err =
+          visitor.Visit(const_cast<clang::Stmt*>(stmt))) {
+    state = LifetimeLattice(*err);
+  }
+}
+
+namespace {
+
+std::optional<std::string> TransferStmtVisitor::VisitExpr(
+    const clang::Expr* expr) {
+  // Ensure that we don't attempt to analyze code that contains errors.
+  // This is triggered by TypoExpr and RecoveryExpr, but rather than handling
+  // these particular expression types individually, we just check
+  // Expr::containsErrors().
+  if (expr->containsErrors()) {
+    return "encountered an expression containing errors";
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitDeclRefExpr(
+    const clang::DeclRefExpr* decl_ref) {
+  auto* decl = decl_ref->getDecl();
+  if (!clang::isa<clang::VarDecl>(decl) &&
+      !clang::isa<clang::FunctionDecl>(decl)) {
+    return std::nullopt;
+  }
+
+  Object object = object_repository_.GetDeclObject(decl);
+
+  assert(decl_ref->isGLValue() || decl_ref->getType()->isBuiltinType());
+
+  clang::QualType type = decl->getType().getCanonicalType();
+
+  if (type->isReferenceType()) {
+    points_to_map_.SetExprObjectSet(
+        decl_ref, points_to_map_.GetPointerPointsToSet(object));
+  } else {
+    points_to_map_.SetExprObjectSet(decl_ref, {object});
+  }
+
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitStringLiteral(
+    const clang::StringLiteral* strlit) {
+  Object obj = object_repository_.CreateStaticObject(strlit->getType());
+  points_to_map_.SetExprObjectSet(strlit, {obj});
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitCastExpr(
+    const clang::CastExpr* cast) {
+  switch (cast->getCastKind()) {
+    case clang::CK_LValueToRValue: {
+      if (cast->getType()->isPointerType()) {
+        // Converting from a glvalue to a prvalue means that we need to perform
+        // a dereferencing operation because the objects associated with
+        // glvalues and prvalues have different meanings:
+        // - A glvalue is associated with the object identified by the glvalue.
+        // - A prvalue is only associated with an object if the prvalue is of
+        //   pointer type; the object it is associated with is the object the
+        //   pointer points to.
+        // See also documentation for PointsToMap.
+        ObjectSet points_to = points_to_map_.GetPointerPointsToSet(
+            points_to_map_.GetExprObjectSet(cast->getSubExpr()));
+        points_to_map_.SetExprObjectSet(cast, points_to);
+      }
+      break;
+    }
+    case clang::CK_NullToPointer: {
+      points_to_map_.SetExprObjectSet(cast, {});
+      break;
+    }
+    // These casts are just no-ops from a Object point of view.
+    case clang::CK_FunctionToPointerDecay:
+    case clang::CK_BuiltinFnToFnPtr:
+    case clang::CK_ArrayToPointerDecay:
+    case clang::CK_UserDefinedConversion:
+      // Note on CK_UserDefinedConversion: The actual conversion happens in a
+      // CXXMemberCallExpr that is a subexpression of this CastExpr. The
+      // CK_UserDefinedConversion is just used to mark the fact that this is a
+      // user-defined conversion; it's therefore a no-op for our purposes.
+    case clang::CK_NoOp: {
+      clang::QualType type = cast->getType().getCanonicalType();
+      if (type->isPointerType() || cast->isGLValue()) {
+        points_to_map_.SetExprObjectSet(
+            cast, points_to_map_.GetExprObjectSet(cast->getSubExpr()));
+      }
+      break;
+    }
+    case clang::CK_DerivedToBase:
+    case clang::CK_UncheckedDerivedToBase:
+    case clang::CK_BaseToDerived:
+    case clang::CK_Dynamic: {
+      // These need to be mapped to what the subexpr points to.
+      // (Simple cases just work okay with this; may need to be revisited when
+      // we add more inheritance support.)
+      ObjectSet points_to = points_to_map_.GetExprObjectSet(cast->getSubExpr());
+      points_to_map_.SetExprObjectSet(cast, points_to);
+      break;
+    }
+    case clang::CK_BitCast:
+    case clang::CK_LValueBitCast:
+    case clang::CK_IntegralToPointer: {
+      // We don't support analyzing functions that perform a reinterpret_cast.
+      diag_reporter_(
+          func_->getBeginLoc(),
+          "cannot infer lifetimes because function uses a type-unsafe cast",
+          clang::DiagnosticIDs::Warning);
+      diag_reporter_(cast->getBeginLoc(), "type-unsafe cast occurs here",
+                     clang::DiagnosticIDs::Note);
+      return "type-unsafe cast prevents analysis";
+    }
+    default: {
+      if (cast->isGLValue() ||
+          cast->getType().getCanonicalType()->isPointerType()) {
+        llvm::errs() << "Unknown cast type:\n";
+        cast->dump();
+        // No-noop casts of pointer types are not handled yet.
+        llvm::report_fatal_error("unknown cast type encountered");
+      }
+    }
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitReturnStmt(
+    const clang::ReturnStmt* return_stmt) {
+  clang::QualType return_type = func_->getReturnType();
+  // We only need to handle pointers and references.
+  // For record types, initialization of the return value has already been
+  // handled in VisitCXXConstructExpr() or VisitInitListExpr(), so nothing
+  // to do here.
+  if (!return_type->isPointerType() && !return_type->isReferenceType()) {
+    return std::nullopt;
+  }
+
+  const clang::Expr* ret_expr = return_stmt->getRetValue();
+  // This occurs when computing `ret_expr`s result includes creating temporary
+  // objects with destructors. We want to find the value to be returned inside
+  // the ExprWithCleanups.
+  //
+  // The PointsToMap::GetExprObjectSet() function could do this but it doesn't
+  // understand the context from which it is being called. This operation needs
+  // to be done only in cases where we are leaving scope - that is, the return
+  // statement. And the return statement also needs to look for initializers in
+  // its sub expressions, after looking inside ExprWithCleanups.
+  //
+  // That means GetExprObjectSet() would need to also look for initializers but
+  // we don't want to do this on every call to GetExprObjectSet().
+  if (auto cleanups = clang::dyn_cast<clang::ExprWithCleanups>(ret_expr)) {
+    ret_expr = cleanups->getSubExpr();
+  }
+
+  ObjectSet expr_points_to = points_to_map_.GetExprObjectSet(ret_expr);
+  points_to_map_.ExtendPointerPointsToSet(object_repository_.GetReturnObject(),
+                                          expr_points_to);
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitDeclStmt(
+    const clang::DeclStmt* decl_stmt) {
+  for (const clang::Decl* decl : decl_stmt->decls()) {
+    if (const auto* var_decl = clang::dyn_cast<clang::VarDecl>(decl)) {
+      Object var_object = object_repository_.GetDeclObject(var_decl);
+
+      // Don't need to record initializers because initialization has already
+      // happened in VisitCXXConstructExpr(), VisitInitListExpr(), or
+      // VisitCallExpr().
+      if (var_decl->hasInit() && !var_decl->getType()->isRecordType()) {
+        TransferInitializer(var_object, var_decl->getType(), object_repository_,
+                            var_decl->getInit(), points_to_map_);
+      }
+    }
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitUnaryOperator(
+    const clang::UnaryOperator* op) {
+  if (!op->isGLValue() && !op->getType()->isPointerType() &&
+      !op->getType()->isArrayType()) {
+    return std::nullopt;
+  }
+
+  ObjectSet sub_points_to = points_to_map_.GetExprObjectSet(op->getSubExpr());
+
+  // Maybe surprisingly, the code here doesn't do any actual address-taking or
+  // dereferencing.
+  // This is because AddrOf and Deref really only do a reinterpretation:
+  // - AddrOf reinterprets a glvalue of type T as a prvalue of type T*
+  // - Deref reinterprets an prvalue of type T* as a glvalue of type T
+  // (See also the assertions below.)
+  // The actual dereferencing happens in the LValueToRValue CastExpr,
+  // see TransferCastExpr().
+
+  switch (op->getOpcode()) {
+    case clang::UO_AddrOf:
+      assert(!op->isGLValue());
+      assert(op->getSubExpr()->isGLValue());
+      points_to_map_.SetExprObjectSet(op, sub_points_to);
+      break;
+
+    case clang::UO_Deref:
+      assert(op->isGLValue());
+      assert(!op->getSubExpr()->isGLValue());
+      points_to_map_.SetExprObjectSet(op, sub_points_to);
+      break;
+
+    case clang::UO_PostInc:
+    case clang::UO_PostDec:
+      assert(!op->isGLValue());
+      assert(op->getSubExpr()->isGLValue());
+      points_to_map_.SetExprObjectSet(
+          op, points_to_map_.GetPointerPointsToSet(sub_points_to));
+      break;
+
+    case clang::UO_PreInc:
+    case clang::UO_PreDec:
+      assert(op->isGLValue());
+      assert(op->getSubExpr()->isGLValue());
+      points_to_map_.SetExprObjectSet(op, sub_points_to);
+      break;
+
+    default:
+      break;
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitArraySubscriptExpr(
+    const clang::ArraySubscriptExpr* subscript) {
+  // For our purposes here, a subscripting operation is equivalent to a
+  // dereference on its base - we don't make a distinction between different
+  // lifetimes in an array. This effectively merges the points-to sets of all
+  // elements in the array. See <internal link> for why we
+  // don't track individual array elements.
+
+  ObjectSet sub_points_to =
+      points_to_map_.GetExprObjectSet(subscript->getBase());
+
+  assert(subscript->isGLValue());
+  assert(!subscript->getBase()->isGLValue());
+  points_to_map_.SetExprObjectSet(subscript, sub_points_to);
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitBinaryOperator(
+    const clang::BinaryOperator* op) {
+  switch (op->getOpcode()) {
+    case clang::BO_Assign: {
+      assert(op->getLHS()->isGLValue());
+      ObjectSet lhs_points_to = points_to_map_.GetExprObjectSet(op->getLHS());
+      points_to_map_.SetExprObjectSet(op, lhs_points_to);
+      // Because of how we handle reference-like structs, a member access to a
+      // non-reference-like field in a struct might still produce lifetimes. We
+      // don't want to change points-to sets in those cases.
+      if (!op->getLHS()->getType()->isPointerType()) break;
+      ObjectSet rhs_points_to = points_to_map_.GetExprObjectSet(op->getRHS());
+      for (Object pointer : lhs_points_to) {
+        if (object_repository_.GetObjectValueType(pointer) ==
+            ObjectRepository::ObjectValueType::kMultiValued) {
+          points_to_map_.ExtendPointerPointsToSet(pointer, rhs_points_to);
+        } else {
+          points_to_map_.SetPointerPointsToSet(pointer, rhs_points_to);
+        }
+      }
+      break;
+    }
+
+    case clang::BO_Add:
+    case clang::BO_Sub: {
+      // Pointer arithmetic.
+      // We are only interested in the case in which exactly one of the two
+      // operands is a pointer (in particular we want to exclude int* - int*).
+      if (op->getLHS()->getType()->isPointerType() ^
+          op->getRHS()->getType()->isPointerType()) {
+        if (op->getLHS()->getType()->isPointerType()) {
+          points_to_map_.SetExprObjectSet(
+              op, points_to_map_.GetExprObjectSet(op->getLHS()));
+        } else {
+          points_to_map_.SetExprObjectSet(
+              op, points_to_map_.GetExprObjectSet(op->getRHS()));
+        }
+      }
+      break;
+    }
+
+    default:
+      break;
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitConditionalOperator(
+    const clang::ConditionalOperator* op) {
+  clang::QualType type = op->getType().getCanonicalType();
+
+  if (op->isGLValue() || type->isPointerType()) {
+    ObjectSet points_to_true =
+        points_to_map_.GetExprObjectSet(op->getTrueExpr());
+    ObjectSet points_to_false =
+        points_to_map_.GetExprObjectSet(op->getFalseExpr());
+    points_to_map_.SetExprObjectSet(op, points_to_true.Union(points_to_false));
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitInitListExpr(
+    const clang::InitListExpr* init_list) {
+  if (init_list->isSyntacticForm()) {
+    // We are only interested in the semantic form, which is fully realized,
+    // and is the one considered to be the initializer.
+    return std::nullopt;
+  }
+  if (IsInitExprInitializingARecordObject(init_list)) {
+    if (init_list->isTransparent()) {
+      // A transparent initializer list does nothing, the actual initializer
+      // terminating expression is within, and has already transferred lifetimes
+      // up to the object being initialized.
+      return std::nullopt;
+    }
+    // The object set for each field should be pointing to the initializers.
+    Object init_object = object_repository_.GetInitializedObject(init_list);
+    TransferInitializer(init_object, init_list->getType(), object_repository_,
+                        init_list, points_to_map_);
+  } else {
+    // If the InitListExpr is not initializing a record object, we assume it's
+    // initializing an array or a reference and hence associate the InitListExpr
+    // with the union of the points-to sets of the initializers (as the analysis
+    // is array-insensitive).
+    ObjectSet targets;
+    for (clang::Expr* expr : init_list->inits()) {
+      // If we are constructing an initializer list of non-pointer types, we
+      // don't need to do anything here. Note that initializer list elements
+      // must all have the same type in this case.
+      if (PointeeType(expr->getType()).isNull() && !expr->isGLValue()) {
+        return std::nullopt;
+      }
+      targets.Add(points_to_map_.GetExprObjectSet(expr));
+    }
+    points_to_map_.SetExprObjectSet(init_list, std::move(targets));
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitMaterializeTemporaryExpr(
+    const clang::MaterializeTemporaryExpr* temporary_expr) {
+  Object temp_object = object_repository_.GetTemporaryObject(temporary_expr);
+  points_to_map_.SetExprObjectSet(temporary_expr, {temp_object});
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitMemberExpr(
+    const clang::MemberExpr* member) {
+  ObjectSet struct_points_to =
+      points_to_map_.GetExprObjectSet(member->getBase());
+
+  if (const auto* method =
+          clang::dyn_cast<clang::CXXMethodDecl>(member->getMemberDecl())) {
+    // It doesn't really make sense to associate an object set with a non-static
+    // member function.
+    // If the member function is being called, we're not interested in its
+    // "value" anyway. If the non-static member function is used outside of a
+    // function call, then, it's a pointer-to-member, but those aren't
+    // really pointers anyway, and we'll need special treatment for them.
+    if (method->isStatic()) {
+      points_to_map_.SetExprObjectSet(
+          member, {object_repository_.GetDeclObject(method)});
+    }
+    return std::nullopt;
+  }
+
+  auto field = clang::dyn_cast<clang::FieldDecl>(member->getMemberDecl());
+  if (field == nullptr) {
+    llvm::report_fatal_error("indirect member access is not supported yet");
+  }
+  ObjectSet expr_points_to =
+      object_repository_.GetFieldObject(struct_points_to, field);
+  if (field->getType()->isReferenceType()) {
+    expr_points_to = points_to_map_.GetPointerPointsToSet(expr_points_to);
+  }
+  points_to_map_.SetExprObjectSet(member, expr_points_to);
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitCXXThisExpr(
+    const clang::CXXThisExpr* this_expr) {
+  std::optional<Object> this_object = object_repository_.GetThisObject();
+  assert(this_object.has_value());
+  points_to_map_.SetExprObjectSet(this_expr, ObjectSet{this_object.value()});
+  return std::nullopt;
+}
+
+bool AllStatic(const ValueLifetimes& lifetimes) {
+  return !lifetimes.HasAny([](Lifetime l) { return l != Lifetime::Static(); });
+}
+
+struct FunctionParameter {
+  clang::QualType param_type;
+  ValueLifetimes param_lifetimes;
+  Object arg_object;
+};
+
+// Collects all function parameters, including (if this is a member call) the
+// implicit this argument.
+std::vector<FunctionParameter> CollectFunctionParameters(
+    const clang::CallExpr* call, const clang::FunctionDecl* callee,
+    const FunctionLifetimes& callee_lifetimes,
+    const ObjectRepository& object_repository) {
+  std::vector<FunctionParameter> fn_params;
+
+  if (clang::isa<clang::CXXOperatorCallExpr>(call) &&
+      clang::isa<clang::CXXMethodDecl>(callee)) {
+    // `this` is considered an argument in this case (but not a parameter on its
+    // definition).
+    assert(call->getNumArgs() == callee->getNumParams() + 1);
+
+    // Handle the `this` argument.
+    {
+      fn_params.push_back(FunctionParameter{
+          clang::dyn_cast<clang::CXXMethodDecl>(callee)->getThisType(),
+          callee_lifetimes.GetThisLifetimes(),
+          object_repository.GetCallExprThisPointer(call)});
+    }
+
+    // Handle all other arguments.
+    for (size_t i = 1; i < call->getNumArgs(); i++) {
+      fn_params.push_back(FunctionParameter{
+          callee->getParamDecl(i - 1)->getType().getCanonicalType(),
+          callee_lifetimes.GetParamLifetimes(i - 1),
+          object_repository.GetCallExprArgumentObject(call, i)});
+    }
+  } else {
+    // We check <= instead of == because of default arguments.
+    assert(call->getNumArgs() <= callee->getNumParams());
+
+    for (size_t i = 0; i < call->getNumArgs(); i++) {
+      fn_params.push_back(FunctionParameter{
+          callee->getParamDecl(i)->getType().getCanonicalType(),
+          callee_lifetimes.GetParamLifetimes(i),
+          object_repository.GetCallExprArgumentObject(call, i)});
+    }
+    if (const auto* member_call =
+            clang::dyn_cast<clang::CXXMemberCallExpr>(call)) {
+      // The callee is always a MemberExpr.
+      // - If the call uses `->`, the object argument should be a prvalue that
+      //   is a pointer to the struct.
+      // - If the call uses `.`, the object argument should be a glvalue of
+      //   struct type.
+      assert(clang::isa<clang::MemberExpr>(member_call->getCallee()));
+      assert(clang::dyn_cast<clang::MemberExpr>(member_call->getCallee())
+                 ->isArrow() ^
+             member_call->getImplicitObjectArgument()->isGLValue());
+      // This is the type of the function *parameter*, not of the argument.
+      // This is always a pointer, even if the argument is a reference, but as
+      // we don't treat pointers or references differently, this is not an
+      // issue.
+      fn_params.push_back(
+          FunctionParameter{member_call->getMethodDecl()->getThisType(),
+                            callee_lifetimes.GetThisLifetimes(),
+                            object_repository.GetCallExprThisPointer(call)});
+    }
+  }
+  return fn_params;
+}
+
+void CollectLifetimes(
+    Object arg_object, clang::QualType type,
+    const ValueLifetimes& value_lifetimes, const PointsToMap& points_to_map,
+    const ObjectRepository& object_repository,
+    llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set) {
+  class Visitor : public LifetimeVisitor {
+   public:
+    Visitor(const ObjectRepository& object_repository,
+            const PointsToMap& points_to_map,
+            llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set)
+        : object_repository_(object_repository),
+          points_to_map_(points_to_map),
+          lifetime_points_to_set_(lifetime_points_to_set) {}
+
+    Object GetFieldObject(const ObjectSet& objects,
+                          const clang::FieldDecl* field) override {
+      // All the objects have the same field.
+      assert(!objects.empty());
+      return object_repository_.GetFieldObject(*objects.begin(), field);
+    }
+
+    Object GetBaseClassObject(const ObjectSet& objects,
+                              clang::QualType base) override {
+      // All the objects have the same base.
+      assert(!objects.empty());
+      return object_repository_.GetBaseClassObject(*objects.begin(), base);
+    }
+
+    ObjectSet Traverse(const ObjectLifetimes& lifetimes,
+                       const ObjectSet& objects,
+                       int /*pointee_depth*/) override {
+      lifetime_points_to_set_[lifetimes.GetLifetime()].Add(objects);
+      return points_to_map_.GetPointerPointsToSet(objects);
+    }
+
+   private:
+    const ObjectRepository& object_repository_;
+    const PointsToMap& points_to_map_;
+    llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set_;
+  };
+  Visitor visitor(object_repository, points_to_map, lifetime_points_to_set);
+  VisitLifetimes({arg_object}, type,
+                 ObjectLifetimes(arg_object.GetLifetime(), value_lifetimes),
+                 visitor);
+}
+
+void SetPointerPointsToSetRespectingTypes(Object pointer,
+                                          const ObjectSet& points_to,
+                                          PointsToMap& points_to_map,
+                                          clang::ASTContext& ast_context) {
+  assert(pointer.Type()->isPointerType() || pointer.Type()->isReferenceType());
+
+  ObjectSet points_to_filtered;
+
+  for (auto object : points_to) {
+    if (MayPointTo(pointer.Type(), object.Type(), ast_context)) {
+      points_to_filtered.Add(object);
+    }
+  }
+
+  points_to_map.SetPointerPointsToSet(pointer, points_to_filtered);
+}
+
+void SetAllPointersPointsToSetRespectingTypes(const ObjectSet& pointers,
+                                              const ObjectSet& points_to,
+                                              PointsToMap& points_to_map,
+                                              clang::ASTContext& ast_context) {
+  for (auto pointer : pointers) {
+    SetPointerPointsToSetRespectingTypes(pointer, points_to, points_to_map,
+                                         ast_context);
+  }
+}
+
+void SetExprObjectSetRespectingType(const clang::Expr* expr,
+                                    const ObjectSet& points_to,
+                                    PointsToMap& points_to_map,
+                                    clang::ASTContext& ast_context) {
+  ObjectSet points_to_filtered;
+
+  for (auto object : points_to) {
+    if (expr->isGLValue()) {
+      if (PointeesCompatible(expr->getType(), object.Type(), ast_context)) {
+        points_to_filtered.Add(object);
+      }
+    } else {
+      clang::QualType expr_type = expr->getType();
+      // CXXConstructExpr is a special case -- it is a non-glvalue with the type
+      // of the constructed object itself. Non-pointer, non-glvalue expressions
+      // like this are not usually allowed to be associated with a points-to
+      // set, but CXXConstructExpr is an exception. We need to associate it with
+      // an `Object` representing the newly constructed object so that
+      // TransferInitializer() can then retrieve this object. So we pretend that
+      // the type is actually "pointer to object" to give MayPointTo() what it
+      // expects.
+      //
+      // Note that we will not see clang::InitListExpr here, which is the other
+      // form of initializer along with CXXConstructExpr. That is because we
+      // come here through a "call" and we don't consider an initializer list to
+      // be a "call" or treat it as such.
+      assert(!clang::isa<clang::InitListExpr>(expr));
+      if (clang::isa<clang::CXXConstructExpr>(expr)) {
+        expr_type = ast_context.getPointerType(expr_type);
+      }
+
+      if (MayPointTo(expr_type, object.Type(), ast_context)) {
+        points_to_filtered.Add(object);
+      }
+    }
+  }
+
+  points_to_map.SetExprObjectSet(expr, points_to_filtered);
+}
+
+void PropagateLifetimesToPointees(
+    Object arg_object, clang::QualType type,
+    const ValueLifetimes& value_lifetimes, PointsToMap& points_to_map,
+    ObjectRepository& object_repository,
+    const llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set,
+    clang::ASTContext& ast_context) {
+  class Visitor : public LifetimeVisitor {
+   public:
+    Visitor(ObjectRepository& object_repository, PointsToMap& points_to_map,
+            const llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set,
+            clang::ASTContext& ast_context)
+        : object_repository_(object_repository),
+          points_to_map_(points_to_map),
+          lifetime_points_to_set_(lifetime_points_to_set),
+          ast_context_(ast_context) {}
+
+    Object GetFieldObject(const ObjectSet& objects,
+                          const clang::FieldDecl* field) override {
+      // All the objects have the same field.
+      assert(!objects.empty());
+      return object_repository_.GetFieldObject(*objects.begin(), field);
+    }
+
+    Object GetBaseClassObject(const ObjectSet& objects,
+                              clang::QualType base) override {
+      // All the objects have the same base.
+      assert(!objects.empty());
+      return object_repository_.GetBaseClassObject(*objects.begin(), base);
+    }
+
+    ObjectSet Traverse(const ObjectLifetimes& lifetimes,
+                       const ObjectSet& objects,
+                       int /*pointee_depth*/) override {
+      clang::QualType type = lifetimes.GetValueLifetimes().Type();
+      ObjectSet points_to_original =
+          points_to_map_.GetPointerPointsToSet(objects);
+      if (!type.isConstQualified() && !PointeeType(type).isNull()) {
+        Lifetime pointee_lifetime =
+            lifetimes.GetValueLifetimes().GetPointeeLifetimes().GetLifetime();
+        ObjectSet points_to = lifetime_points_to_set_.lookup(pointee_lifetime);
+        // If this is pointer-to-static, assume the callee can modify it to
+        // point to a static object that we don't know about.
+        if (pointee_lifetime == Lifetime::Static()) {
+          points_to.Add(
+              object_repository_.CreateStaticObject(PointeeType(type)));
+        }
+        SetAllPointersPointsToSetRespectingTypes(objects, points_to,
+                                                 points_to_map_, ast_context_);
+        assert(points_to_map_.GetPointerPointsToSet(objects).Contains(
+            points_to_original));
+      }
+      // Return the original points-to set, not the modified one. The original
+      // points-to set is sufficient because it captures the arguments that
+      // were passed to the function, but it doesn't contain any possibly
+      // spurious edges that may have been inserted by the logic above, which
+      // can reduce the precision of the analysis.
+      return points_to_original;
+    }
+
+   private:
+    ObjectRepository& object_repository_;
+    PointsToMap& points_to_map_;
+    const llvm::DenseMap<Lifetime, ObjectSet>& lifetime_points_to_set_;
+    clang::ASTContext& ast_context_;
+  };
+  Visitor visitor(object_repository, points_to_map, lifetime_points_to_set,
+                  ast_context);
+  VisitLifetimes({arg_object}, type,
+                 ObjectLifetimes(arg_object.GetLifetime(), value_lifetimes),
+                 visitor);
+}
+
+std::optional<ObjectSet> TransferLifetimesForCall(
+    const clang::Expr* call, const std::vector<FunctionParameter>& fn_params,
+    const ValueLifetimes& return_lifetimes, ObjectRepository& object_repository,
+    PointsToMap& points_to_map, clang::ASTContext& ast_context) {
+  // TODO(mboehme): The following description says what we _want_ to do, but
+  // this isn't what we actually do right now. Modify the code so that it
+  // corresponds to the description, then remove this TODO.
+  //
+  // Overall approach:
+  // - Step 1: Find all objects accessible by the callee.
+  //   This means finding all objects transitively accessible from the argument
+  //   pointees passed to the callee. As part of this step, we establish a
+  //   mapping from callee lifetimes to caller lifetimes, which will be used in
+  //   subsequent steps to determine whether a given object (whose lifetime is
+  //   a caller lifetime) has a given callee lifetime. Note that, in general, a
+  //   single callee lifetime may correspond to multiple caller lifetimes.
+  //
+  // - Step 2: Perform all modifications the callee could make to the points-to
+  //   map that are permissible from a lifetime and type system point of view.
+  //   Specifically, for every non-const pointer accessible by the callee:
+  //   - Determine the callee lifetime 'l associated with that pointer.
+  //   - For each object accessible by the callee, determine whether it has
+  //     callee lifetime 'l (using the mapping established in step 1) and
+  //     and whether the type of the pointer is compatible with the type of the
+  //     object. If both of these conditions are met, add an edge from the
+  //     pointer to the object into the points-to map.
+  //   It remains to be explained what "compatible" means above. The most
+  //   principled approach would be to use C++'s strict aliasing rules, but some
+  //   real-world code unfortunately violates the strict aliasing rules.
+  //   Instead, we make the compatibility rule more permissive than strict
+  //   aliasing; we expect we will need some experimentation to achieve a
+  //   good tradeoff between the following considerations:
+  //   - If we make the compatibility rule too strict, we miss some points-to
+  //     edges that may be introduced by real-world code (even though that code
+  //     is in violation of the strict aliasing rule), and the analysis result
+  //     becomes wrong.
+  //   - If we make the compatibility rule too permissive, we allow spurious
+  //     edges in the points-to map, and the analysis result becomes overly
+  //     restrictive.
+  //   We also need to consider that the type returned by Object::Type() might
+  //   not be identical to the actual dynamic type of the object. If the object
+  //   was passed in to the function through a pointer or reference to class
+  //   type, the dynamic type of the object might be a derived class of the
+  //   type we assumed for the object.
+  //
+  // - Step 3: Determine points-to set for the return value.
+  //   This is the set of all objects accessible by the callee that
+  //   - are compatible with the callee's return type, and
+  //   - conform to the lifetime annotations on the return type.
+  //   The latter point means that every object that is transitively reachable
+  //   from the original object has a lifetime that corresponds to the callee
+  //   lifetime implied by the annotation.
+  //
+  // Some additional considerations apply if the callee signature contains the
+  // 'static lifetime, either in the parameters or the return value:
+  // - Any pointer or reference may point to an object of static lifetime. This
+  //   has the following implications:
+  //   - In step 2, when adding edges to the points-to map, we always add edges
+  //     to objects of static lifetime if their type is compatible with the
+  //     type of the pointer.
+  //   - In step 3, an object of static lifetime conforms to any callee lifetime
+  //     if that lifetime occurs in covariant position.
+  // - The callee may have access to objects of static lifetime that are not
+  //   passed as arguments, in addition to the ones that are accessible from the
+  //   arguments.
+  //   Because of this, for any non-const pointer accessible by the callee, we
+  //   add a points-to edge to a newly created static object of the appropriate
+  //   type.
+  //   This does cause us to add a lot of static objects to the graph that we
+  //   do not expect to occur in reality. If this turns out to have undesired
+  //   effects, we could use the following alternative approach as a compromise:
+  //   - In step 2, if the non-const pointer is associated with static lifetime,
+  //     does not already point to an object of static lifetime and would not
+  //     gain an edge to an existing object of static lifetime, create a new
+  //     object of static lifetime and the appropriate type and add an edge
+  //     from the pointer to the newly created object.
+  //   - In step 3, if we obtain an empty points-to set for the return value
+  //     because the return type contains 'static lifetime annotations and the
+  //     existing objects do not conform to these annotations, add newly
+  //     created static objects to the points-to map in suitable places so that
+  //     we can return a non-empty points-to set.
+  //   TODO(mboehme): Investigate whether it's really so bad to add newly
+  //   created static objects in all the places they could theoretically occur.
+  //   If this turns out not to have any adverse effect on the analysis, it
+  //   would be the more principled and simpler thing to do.
+
+  // Step 1: Create mapping from callee lifetimes to points-to sets.
+  llvm::DenseMap<Lifetime, ObjectSet> lifetime_points_to_set;
+  for (auto [type, param_lifetimes, arg_object] : fn_params) {
+    CollectLifetimes({arg_object}, type, param_lifetimes, points_to_map,
+                     object_repository, lifetime_points_to_set);
+  }
+
+  // Step 2: Propagate points-to sets to output parameters.
+  for (auto [type, param_lifetimes, arg_object] : fn_params) {
+    PropagateLifetimesToPointees({arg_object}, type, param_lifetimes,
+                                 points_to_map, object_repository,
+                                 lifetime_points_to_set, ast_context);
+  }
+
+  // Step 3: Determine points-to set for the return value.
+  if (return_lifetimes.HasLifetimes()) {
+    if (IsInitExprInitializingARecordObject(call)) {
+      Object init_object = object_repository.GetInitializedObject(call);
+      PropagateLifetimesToPointees(
+          {init_object}, call->getType(), return_lifetimes, points_to_map,
+          object_repository, lifetime_points_to_set, ast_context);
+    } else {
+      ObjectSet rval_points_to;
+
+      rval_points_to = lifetime_points_to_set.lookup(
+          return_lifetimes.GetPointeeLifetimes().GetLifetime());
+      // If this return value is a pointer-to-static, assume the callee can
+      // return a static object that we don't know about.
+      if (return_lifetimes.GetPointeeLifetimes().GetLifetime() ==
+          Lifetime::Static()) {
+        bool all_static = AllStatic(return_lifetimes);
+        (void)all_static;
+        assert(all_static);
+        rval_points_to.Add(
+            object_repository.CreateStaticObject(PointeeType(call->getType())));
+      }
+      return rval_points_to;
+    }
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitCallExpr(
+    const clang::CallExpr* call) {
+  llvm::SmallVector<const clang::FunctionDecl*> callees;
+
+  const clang::FunctionDecl* direct_callee = call->getDirectCallee();
+  if (direct_callee) {
+    // This code path is needed for non-static member functions, as those don't
+    // have an `Object` for their callees.
+    callees.push_back(direct_callee);
+  } else {
+    const clang::Expr* callee = call->getCallee();
+    for (const auto& object : points_to_map_.GetExprObjectSet(callee)) {
+      const clang::FunctionDecl* func = object.GetFunc();
+      assert(func);
+      callees.push_back(func);
+    }
+  }
+
+  std::optional<ObjectSet> call_points_to;
+
+  for (const auto* callee : callees) {
+    bool is_builtin = callee->getBuiltinID() != 0;
+
+    FunctionLifetimesOrError builtin_callee_lifetimes_or_error;
+    if (is_builtin) {
+      builtin_callee_lifetimes_or_error = GetBuiltinLifetimes(callee);
+    } else {
+      assert(callee_lifetimes_.count(callee->getCanonicalDecl()));
+    }
+    const FunctionLifetimesOrError& callee_lifetimes_or_error =
+        is_builtin ? builtin_callee_lifetimes_or_error
+                   : callee_lifetimes_.lookup(callee->getCanonicalDecl());
+
+    if (!std::holds_alternative<FunctionLifetimes>(callee_lifetimes_or_error)) {
+      return "No lifetimes for callee '" + callee->getNameAsString() + "': " +
+             std::get<FunctionAnalysisError>(callee_lifetimes_or_error).message;
+    }
+    FunctionLifetimes callee_lifetimes =
+        std::get<FunctionLifetimes>(callee_lifetimes_or_error);
+
+    bool is_member_operator = clang::isa<clang::CXXOperatorCallExpr>(call) &&
+                              clang::isa<clang::CXXMethodDecl>(callee);
+    for (size_t i = is_member_operator ? 1 : 0; i < call->getNumArgs(); i++) {
+      // We can't just use SetPointerPointsToSet here because call->getArg(i)
+      // might not have an ObjectSet (for example for integer constants); it
+      // also may be needed for struct initialization.
+      // Note that we don't need to worry about possibly extending the
+      // PointsToSet more than needed, as dataflow analysis relies on points-to
+      // sets never shrinking.
+      TransferInitializer(
+          object_repository_.GetCallExprArgumentObject(call, i),
+          callee->getParamDecl(is_member_operator ? i - 1 : i)->getType(),
+          object_repository_, call->getArg(i), points_to_map_);
+    }
+    if (is_member_operator) {
+      points_to_map_.SetPointerPointsToSet(
+          object_repository_.GetCallExprThisPointer(call),
+          points_to_map_.GetExprObjectSet(call->getArg(0)));
+    }
+    if (const auto* member_call =
+            clang::dyn_cast<clang::CXXMemberCallExpr>(call)) {
+      points_to_map_.SetPointerPointsToSet(
+          object_repository_.GetCallExprThisPointer(call),
+          points_to_map_.GetExprObjectSet(
+              member_call->getImplicitObjectArgument()));
+    }
+
+    std::vector<FunctionParameter> fn_params = CollectFunctionParameters(
+        call, callee, callee_lifetimes, object_repository_);
+
+    std::optional<ObjectSet> single_call_points_to = TransferLifetimesForCall(
+        call, fn_params, callee_lifetimes.GetReturnLifetimes(),
+        object_repository_, points_to_map_, callee->getASTContext());
+    if (single_call_points_to) {
+      if (call_points_to) {
+        call_points_to.value().Add(std::move(single_call_points_to).value());
+      } else {
+        call_points_to = std::move(single_call_points_to);
+      }
+    }
+  }
+
+  if (call_points_to) {
+    SetExprObjectSetRespectingType(call, call_points_to.value(), points_to_map_,
+                                   callees[0]->getASTContext());
+  }
+  return std::nullopt;
+}
+
+std::optional<std::string> TransferStmtVisitor::VisitCXXConstructExpr(
+    const clang::CXXConstructExpr* construct_expr) {
+  const clang::CXXConstructorDecl* constructor =
+      construct_expr->getConstructor();
+
+  assert(callee_lifetimes_.count(constructor->getCanonicalDecl()));
+  const FunctionLifetimesOrError& callee_lifetimes_or_error =
+      callee_lifetimes_.lookup(constructor->getCanonicalDecl());
+  if (!std::holds_alternative<FunctionLifetimes>(callee_lifetimes_or_error)) {
+    return "No lifetimes for constructor " + constructor->getNameAsString();
+  }
+  const FunctionLifetimes& callee_lifetimes =
+      std::get<FunctionLifetimes>(callee_lifetimes_or_error);
+
+  // We check <= instead of == because of default arguments.
+  assert(construct_expr->getNumArgs() <= constructor->getNumParams());
+
+  for (size_t i = 0; i < construct_expr->getNumArgs(); i++) {
+    TransferInitializer(
+        object_repository_.GetCXXConstructExprArgumentObject(construct_expr, i),
+        construct_expr->getArg(i)->getType(), object_repository_,
+        construct_expr->getArg(i), points_to_map_);
+  }
+
+  // Handle the `this` parameter, which should point to the object getting
+  // initialized.
+  points_to_map_.SetPointerPointsToSet(
+      object_repository_.GetCXXConstructExprThisPointer(construct_expr),
+      {object_repository_.GetInitializedObject(construct_expr)});
+
+  // Populate fn_params for the constructor call.
+  std::vector<FunctionParameter> fn_params;
+
+  for (size_t i = 0; i < construct_expr->getNumArgs(); i++) {
+    clang::QualType arg_type =
+        constructor->getParamDecl(i)->getType().getCanonicalType();
+    fn_params.push_back(
+        FunctionParameter{arg_type, callee_lifetimes.GetParamLifetimes(i),
+                          object_repository_.GetCXXConstructExprArgumentObject(
+                              construct_expr, i)});
+  }
+
+  clang::QualType type = constructor->getThisType();
+  fn_params.push_back(FunctionParameter{
+      type, callee_lifetimes.GetThisLifetimes(),
+      object_repository_.GetCXXConstructExprThisPointer(construct_expr)});
+
+  TransferLifetimesForCall(
+      construct_expr, fn_params,
+      ValueLifetimes::ForLifetimeLessType(constructor->getReturnType()),
+      object_repository_, points_to_map_, constructor->getASTContext());
+  return std::nullopt;
+}
+
+}  // namespace
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/lifetime_analysis.h b/lifetime_analysis/lifetime_analysis.h
new file mode 100644
index 0000000..e00f7ca
--- /dev/null
+++ b/lifetime_analysis/lifetime_analysis.h
@@ -0,0 +1,75 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_ANALYSIS_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_ANALYSIS_H_
+
+#include <functional>
+#include <string>
+#include <variant>
+
+#include "lifetime_analysis/lifetime_lattice.h"
+#include "lifetime_analysis/object_repository.h"
+#include "lifetime_analysis/points_to_map.h"
+#include "lifetime_annotations/function_lifetimes.h"
+#include "lifetime_annotations/lifetime.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Type.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/raw_ostream.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+void TransferInitializer(Object dest, clang::QualType type,
+                         const ObjectRepository& object_repository,
+                         const clang::Expr* init_expr,
+                         PointsToMap& points_to_map);
+
+// Function to call to report a diagnostic.
+// This has the same interface as ClangTidyCheck::diag().
+using DiagnosticReporter = std::function<clang::DiagnosticBuilder(
+    clang::SourceLocation, clang::StringRef, clang::DiagnosticIDs::Level)>;
+
+class LifetimeAnalysis
+    : public clang::dataflow::DataflowAnalysis<LifetimeAnalysis,
+                                               LifetimeLattice> {
+ public:
+  explicit LifetimeAnalysis(
+      const clang::FunctionDecl* func, ObjectRepository& object_repository,
+      const llvm::DenseMap<const clang::FunctionDecl*,
+                           FunctionLifetimesOrError>& callee_lifetimes,
+      const DiagnosticReporter& diag_reporter)
+      : clang::dataflow::DataflowAnalysis<LifetimeAnalysis, LifetimeLattice>(
+            func->getASTContext(), /*ApplyBuiltinTransfer=*/false),
+        func_(func),
+        object_repository_(object_repository),
+        callee_lifetimes_(callee_lifetimes),
+        diag_reporter_(diag_reporter) {}
+
+  LifetimeLattice initialElement();
+
+  std::string ToString(const LifetimeLattice& state);
+
+  bool IsEqual(const LifetimeLattice& state1, const LifetimeLattice& state2);
+
+  void transfer(const clang::Stmt* stmt, LifetimeLattice& state,
+                clang::dataflow::Environment& environment);
+
+ private:
+  const clang::FunctionDecl* func_;
+  ObjectRepository& object_repository_;
+  const llvm::DenseMap<const clang::FunctionDecl*, FunctionLifetimesOrError>&
+      callee_lifetimes_;
+  const DiagnosticReporter& diag_reporter_;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_ANALYSIS_H_
diff --git a/lifetime_analysis/lifetime_lattice.cc b/lifetime_analysis/lifetime_lattice.cc
new file mode 100644
index 0000000..03478d0
--- /dev/null
+++ b/lifetime_analysis/lifetime_lattice.cc
@@ -0,0 +1,75 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "lifetime_analysis/lifetime_lattice.h"
+
+#include <assert.h>
+
+#include <string>
+#include <tuple>
+#include <utility>
+
+#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
+#include "llvm/Support/ErrorHandling.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+std::string LifetimeLattice::ToString() const {
+  if (IsError()) {
+    return Error().str();
+  }
+  return PointsTo().DebugString();
+}
+
+PointsToMap& LifetimeLattice::PointsTo() {
+  assert(!IsError());
+  return std::get<PointsToMap>(var_);
+}
+
+const PointsToMap& LifetimeLattice::PointsTo() const {
+  assert(!IsError());
+  return std::get<PointsToMap>(var_);
+}
+
+llvm::StringRef LifetimeLattice::Error() const {
+  assert(IsError());
+  if (!IsError()) {
+    llvm::report_fatal_error(
+        "Trying to access error on non-error LifetimeLattice");
+  }
+  return std::get<std::string>(var_);
+}
+
+clang::dataflow::LatticeJoinEffect LifetimeLattice::join(
+    const LifetimeLattice& other) {
+  if (IsError()) {
+    return clang::dataflow::LatticeJoinEffect::Unchanged;
+  }
+  if (other.IsError()) {
+    *this = other;
+    return clang::dataflow::LatticeJoinEffect::Changed;
+  }
+
+  PointsToMap joined_points_to_map = PointsTo().Union(other.PointsTo());
+  if (PointsTo() == joined_points_to_map) {
+    return clang::dataflow::LatticeJoinEffect::Unchanged;
+  }
+
+  *this = LifetimeLattice(std::move(joined_points_to_map));
+  return clang::dataflow::LatticeJoinEffect::Changed;
+}
+
+bool LifetimeLattice::operator==(const LifetimeLattice& other) const {
+  if (IsError() || other.IsError()) {
+    // Any error compares equal to any other error.
+    return IsError() && other.IsError();
+  }
+  return PointsTo() == other.PointsTo();
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/lifetime_lattice.h b/lifetime_analysis/lifetime_lattice.h
new file mode 100644
index 0000000..68e8a0e
--- /dev/null
+++ b/lifetime_analysis/lifetime_lattice.h
@@ -0,0 +1,74 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#ifndef DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_LATTICE_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_LATTICE_H_
+
+#include <string>
+#include <utility>
+#include <variant>
+
+#include "lifetime_analysis/points_to_map.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+#include "clang/Analysis/FlowSensitive/DataflowLattice.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+class LifetimeLattice {
+ public:
+  // Creates a lattice holding an empty points-to map.
+  LifetimeLattice() = default;
+
+  LifetimeLattice(const LifetimeLattice&) = default;
+  LifetimeLattice(LifetimeLattice&&) = default;
+  LifetimeLattice& operator=(const LifetimeLattice&) = default;
+  LifetimeLattice& operator=(LifetimeLattice&&) = default;
+
+  // Creates a lattice containing the given points-to map.
+  explicit LifetimeLattice(PointsToMap points_to_map)
+      : var_(std::move(points_to_map)) {}
+
+  // Creates an error state containing the error message `err`.
+  explicit LifetimeLattice(std::string err) : var_(err) {}
+
+  // Returns the points-to map.
+  // Precondition: !IsError().
+  PointsToMap& PointsTo();
+  const PointsToMap& PointsTo() const;
+
+  // Returns whether the lattice is in the error state.
+  bool IsError() const { return std::holds_alternative<std::string>(var_); }
+
+  // Returns the error string.
+  // Precondition: IsError().
+  llvm::StringRef Error() const;
+
+  // Returns a human-readable representation of the lattice.
+  std::string ToString() const;
+
+  // Sets the lattice to the result of the "join" operation with `other` and
+  // returns the effect of the operation.
+  // If either of the lattices contains an error, sets this lattice to the
+  // first error encountered.
+  clang::dataflow::LatticeJoinEffect join(const LifetimeLattice& other);
+
+  // Compares for (in-)equality.
+  // All error states are considered to be equal.
+  bool operator==(const LifetimeLattice& other) const;
+  bool operator!=(const LifetimeLattice& other) const {
+    return !(*this == other);
+  }
+
+ private:
+  std::variant<PointsToMap, std::string> var_;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_LIFETIME_LATTICE_H_
diff --git a/lifetime_analysis/object.cc b/lifetime_analysis/object.cc
new file mode 100644
index 0000000..c7ca37a
--- /dev/null
+++ b/lifetime_analysis/object.cc
@@ -0,0 +1,75 @@
+// Part of the Crubit project, under the Apache License v2.0 with LLVM
+// Exceptions. See /LICENSE for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+#include "lifetime_analysis/object.h"
+
+#include <string>
+
+#include "absl/strings/str_cat.h"
+#include "lifetime_annotations/lifetime.h"
+#include "clang/AST/Decl.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+constexpr int INVALID_OBJECT_ID_EMPTY = 0;
+constexpr int INVALID_OBJECT_ID_TOMBSTONE = 1;
+constexpr int FIRST_OBJECT_ID = 2;
+
+std::atomic<int> Object::next_id_{FIRST_OBJECT_ID};
+
+Object::Object() : id_(INVALID_OBJECT_ID_EMPTY) {}
+
+Object Object::Create(Lifetime lifetime, clang::QualType type) {
+  assert(!type.isNull());
+  return Object(next_id_++, lifetime, type);
+}
+
+Object Object::CreateFromFunctionDecl(const clang::FunctionDecl& func) {
+  Object ret = Create(Lifetime::Static(), func.getType());
+  ret.func_ = &func;
+  return ret;
+}
+
+std::string Object::DebugString() const {
+  assert(IsValid());
+
+  switch (id_) {
+    case INVALID_OBJECT_ID_EMPTY:
+      return "INVALID_EMPTY";
+    case INVALID_OBJECT_ID_TOMBSTONE:
+      return "INVALID_TOMBSTONE";
+    default: {
+      std::string result = absl::StrCat("p", id_, " ", lifetime_.DebugString());
+      if (!type_.isNull()) {
+        absl::StrAppend(&result, " (", type_.getAsString(), ")");
+      }
+      return result;
+    }
+  }
+}
+
+Object::Object(int id, Lifetime lifetime, clang::QualType type)
+    : id_(id), lifetime_(lifetime), type_(type), func_(nullptr) {}
+
+Object Object::InvalidEmpty() {
+  return Object(INVALID_OBJECT_ID_EMPTY, Lifetime(), clang::QualType());
+}
+
+Object Object::InvalidTombstone() {
+  return Object(INVALID_OBJECT_ID_TOMBSTONE, Lifetime(), clang::QualType());
+}
+
+bool Object::IsValid() const {
+  return id_ != INVALID_OBJECT_ID_EMPTY && id_ != INVALID_OBJECT_ID_TOMBSTONE;
+}
+
+std::ostream& operator<<(std::ostream& os, Object object) {
+  return os << object.DebugString();
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/object.h b/lifetime_analysis/object.h
new file mode 100644
index 0000000..bd80346
--- /dev/null
+++ b/lifetime_analysis/object.h
@@ -0,0 +1,118 @@
+// 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_OBJECT_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_H_
+
+#include <atomic>
+#include <functional>
+#include <string>
+
+#include "lifetime_annotations/lifetime.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Type.h"
+#include "llvm/ADT/Hashing.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// Any object that has a lifetime. Multiple objects might have the same
+// lifetime, but two equal objects always have the same lifetime.
+class Object {
+ public:
+  // Creates an invalid object.
+  //
+  // This is provided because containers need default constructors. It is not
+  // legal to perform any operations on an invalid object except to copy or
+  // delete it.
+  //
+  // Use one of the static member functions below to create a valid object.
+  Object();
+
+  Object(const Object&) = default;
+  Object& operator=(const Object&) = default;
+
+  // Creates a new object with the given lifetime and type.
+  static Object Create(Lifetime lifetime, clang::QualType type);
+
+  // Creates a new object representing a declared function.
+  static Object CreateFromFunctionDecl(const clang::FunctionDecl& func);
+
+  // Returns the lifetime of the object.
+  Lifetime GetLifetime() const { return lifetime_; }
+
+  clang::QualType Type() const { return type_; }
+
+  // Returns a textual representation of the object for debug logging.
+  std::string DebugString() const;
+
+  // Returns the function that this object represents, if any.
+  const clang::FunctionDecl* GetFunc() const { return func_; }
+
+  bool operator==(Object other) const { return id_ == other.id_; }
+
+  bool operator!=(Object other) const { return !(*this == other); }
+
+ private:
+  Object(int id, Lifetime lifetime, clang::QualType type);
+
+  bool IsValid() const;
+
+  static Object InvalidEmpty();
+  static Object InvalidTombstone();
+
+  friend class llvm::DenseMapInfo<Object>;
+  friend class std::less<Object>;
+
+  int id_;
+  Lifetime lifetime_;
+  clang::QualType type_;
+  const clang::FunctionDecl* func_;
+  static std::atomic<int> next_id_;
+};
+
+std::ostream& operator<<(std::ostream& os, Object object);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+namespace llvm {
+
+template <>
+struct DenseMapInfo<clang::tidy::lifetimes::Object> {
+  static clang::tidy::lifetimes::Object getEmptyKey() {
+    return clang::tidy::lifetimes::Object::InvalidEmpty();
+  }
+
+  static clang::tidy::lifetimes::Object getTombstoneKey() {
+    return clang::tidy::lifetimes::Object::InvalidTombstone();
+  }
+
+  static unsigned getHashValue(clang::tidy::lifetimes::Object object) {
+    return llvm::hash_value(object.id_);
+  }
+
+  static bool isEqual(clang::tidy::lifetimes::Object lhs,
+                      clang::tidy::lifetimes::Object rhs) {
+    return lhs == rhs;
+  }
+};
+
+}  // namespace llvm
+
+namespace std {
+
+template <>
+struct less<clang::tidy::lifetimes::Object> {
+  bool operator()(const clang::tidy::lifetimes::Object& p1,
+                  const clang::tidy::lifetimes::Object& p2) const {
+    return p1.id_ < p2.id_;
+  }
+};
+
+}  // namespace std
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_H_
diff --git a/lifetime_analysis/object_repository.cc b/lifetime_analysis/object_repository.cc
new file mode 100644
index 0000000..a68af64
--- /dev/null
+++ b/lifetime_analysis/object_repository.cc
@@ -0,0 +1,866 @@
+// 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/object_repository.h"
+
+#include <functional>
+#include <optional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "lifetime_analysis/object.h"
+#include "lifetime_analysis/visit_lifetimes.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/pointee_type.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/AST/Type.h"
+#include "clang/Basic/LLVM.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/ErrorHandling.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+class ObjectRepository::VarDeclVisitor
+    : public clang::RecursiveASTVisitor<VarDeclVisitor> {
+ public:
+  explicit VarDeclVisitor(ObjectRepository& object_repository)
+      : object_repository_(object_repository) {}
+
+  // We need to visit implicitly-defined constructors and assignment operators.
+  bool shouldVisitImplicitCode() { return true; }
+
+  bool VisitVarDecl(clang::VarDecl* var) {
+    // Add objects for any local variables declared in this function.
+    AddObjectForVar(var);
+    return true;
+  }
+
+  bool VisitReturnStmt(clang::ReturnStmt* stmt) {
+    const clang::Expr* expr = stmt->getRetValue();
+    if (IsInitExprInitializingARecordObject(expr)) {
+      PropagateInitializedObject(expr, object_repository_.return_object_);
+    }
+    return true;
+  }
+
+  bool VisitMemberExpr(clang::MemberExpr* member) {
+    if (auto* method =
+            clang::dyn_cast<clang::CXXMethodDecl>(member->getMemberDecl());
+        method && method->isStatic()) {
+      // Create objects for static member functions.
+      AddObjectForFunc(method);
+    }
+    return true;
+  }
+
+  bool VisitDeclRefExpr(clang::DeclRefExpr* decl_ref) {
+    // Add objects for any global variables referenced in this function.
+    // This also runs for local variables, but we don't have to treat those
+    // differently as AddObjectForVar() protects against duplication.
+    if (auto* var_decl = clang::dyn_cast<clang::VarDecl>(decl_ref->getDecl())) {
+      AddObjectForVar(var_decl);
+    }
+    // Add objects for any function referenced in this function.
+    if (auto* function_decl =
+            clang::dyn_cast<clang::FunctionDecl>(decl_ref->getDecl())) {
+      AddObjectForFunc(function_decl);
+    }
+    return true;
+  }
+
+  bool VisitObjCMessageExpr(clang::ObjCMessageExpr* msg_expr) {
+    // ObjCMessageExpr is an initializer expression terminator, so we should
+    // have walked down from the object which requires initialization to find
+    // its terminating expressions, which should have found this expression and
+    // connected it to that object already.
+    if (!object_repository_.initialized_objects_.count(msg_expr)) {
+      msg_expr->dump();
+      llvm::report_fatal_error(
+          "Missing initializer for ObjCMessageExpr, we did not record it "
+          "when we visited something earlier in the tree yet?");
+    }
+    return true;
+  }
+
+  // Create objects for function call arguments.
+  bool VisitCallExpr(clang::CallExpr* call_expr) {
+    if (IsInitExprInitializingARecordObject(call_expr)) {
+      assert(InitializedObjectWasPropagatedTo(call_expr));
+    }
+
+    // For calls to members, the type of the callee is a "bound member function
+    // type", so we look at the declaration instead.
+    if (auto member_call =
+            clang::dyn_cast<clang::CXXMemberCallExpr>(call_expr)) {
+      const clang::FunctionDecl* callee = call_expr->getDirectCallee();
+      // TODO(veluca): pointers-to-members are not supported (yet?)
+      assert(callee);
+      AddObjectsForArguments(call_expr, callee->getType(),
+                             /*index_shift=*/0);
+      auto method = clang::cast<clang::CXXMethodDecl>(callee);
+      clang::QualType type = method->getThisType();
+      object_repository_.call_expr_this_pointers_[call_expr] =
+          CreateLocalObject(type);
+    } else if (auto op_call =
+                   clang::dyn_cast<clang::CXXOperatorCallExpr>(call_expr)) {
+      const clang::FunctionDecl* callee = call_expr->getDirectCallee();
+      auto method = clang::dyn_cast<clang::CXXMethodDecl>(callee);
+      AddObjectsForArguments(call_expr, callee->getType(),
+                             /*index_shift=*/method ? 1 : 0);
+      if (method) {
+        clang::QualType type = method->getThisType();
+        object_repository_.call_expr_this_pointers_[call_expr] =
+            CreateLocalObject(type);
+      }
+    } else {
+      // Always a function pointer.
+      clang::QualType callee_type = call_expr->getCallee()->getType();
+      AddObjectsForArguments(call_expr, callee_type, /*index_shift=*/0);
+    }
+
+    return true;
+  }
+
+  bool VisitCXXConstructExpr(clang::CXXConstructExpr* construct_expr) {
+    assert(InitializedObjectWasPropagatedTo(construct_expr));
+
+    // Create objects for constructor arguments.
+    const clang::FunctionDecl* constructor = construct_expr->getConstructor();
+    AddObjectsForArguments(construct_expr, constructor->getType(),
+                           /*index_shift=*/0);
+    clang::QualType type = construct_expr->getConstructor()->getThisType();
+    object_repository_.call_expr_this_pointers_[construct_expr] =
+        CreateLocalObject(type);
+    return true;
+  }
+
+  bool VisitInitListExpr(clang::InitListExpr* init_list_expr) {
+    // We only want to visit in Semantic form, we ignore Syntactic form.
+    if (IsInitExprInitializingARecordObject(init_list_expr) &&
+        init_list_expr->isSemanticForm() && !init_list_expr->isTransparent()) {
+      assert(InitializedObjectWasPropagatedTo(init_list_expr));
+    }
+    return true;
+  }
+
+  bool VisitMaterializeTemporaryExpr(
+      clang::MaterializeTemporaryExpr* temporary_expr) {
+    object_repository_.temporary_objects_[temporary_expr] =
+        AddTemporaryObjectForExpression(temporary_expr->getSubExpr());
+    return true;
+  }
+
+  bool VisitCompoundStmt(clang::CompoundStmt* compound) {
+    // Create temporary objects for any top-level `CXXTemporaryObjectExpr`s,
+    // i.e. ones that are used as statements.
+    for (clang::Stmt* stmt : compound->body()) {
+      if (auto* temporary = clang::dyn_cast<CXXTemporaryObjectExpr>(stmt)) {
+        AddTemporaryObjectForExpression(temporary);
+      }
+    }
+    return true;
+  }
+
+  Object CreateLocalObject(clang::QualType type) {
+    Object object = Object::Create(Lifetime::CreateLocal(), type);
+    object_repository_.CreateObjects(
+        object, type,
+        [](clang::QualType, llvm::StringRef) {
+          return Lifetime::CreateVariable();
+        },
+        /*transitive=*/false);
+    return object;
+  }
+
+  void AddObjectsForArguments(const clang::Expr* expr,
+                              clang::QualType callee_type, size_t index_shift) {
+    if (callee_type->isDependentType()) {
+      // TODO(veluca): the fact that we reach this point is a clang bug: it
+      // should not be possible to reach dependent types from a template
+      // instantiation. See also the following discussion, where richardsmith@
+      // agrees this looks like a Clang bug and suggests how it might be fixed:
+      // https://chat.google.com/room/AAAAb6i7WDQ/OvLC9NgO91A
+      return;
+    }
+    if (callee_type->isPointerType()) {
+      callee_type = callee_type->getPointeeType();
+    }
+    // TODO(veluca): figure out how to create a test where the callee is a
+    // ParenType.
+    // For reference, this was triggered in the implementation of `bsearch`.
+    callee_type = callee_type.IgnoreParens();
+    assert(callee_type->isFunctionType());
+    // TODO(veluca): could this be a clang::FunctionNoProtoType??
+    const auto* fn_type = clang::cast<clang::FunctionProtoType>(callee_type);
+    for (size_t i = 0; i < fn_type->getNumParams(); ++i) {
+      object_repository_
+          .call_expr_args_objects_[std::make_pair(expr, i + index_shift)] =
+          CreateLocalObject(fn_type->getParamType(i));
+    }
+  }
+
+  void AddObjectForVar(clang::VarDecl* var) {
+    if (object_repository_.object_repository_.count(var)) {
+      return;
+    }
+
+    Lifetime lifetime;
+    LifetimeFactory lifetime_factory;
+
+    switch (var->getStorageClass()) {
+      case clang::SC_Extern:
+      case clang::SC_Static:
+      case clang::SC_PrivateExtern:
+        lifetime = Lifetime::Static();
+        lifetime_factory = [](clang::QualType, llvm::StringRef) {
+          return Lifetime::Static();
+        };
+        break;
+      default:
+        lifetime = Lifetime::CreateLocal();
+        lifetime_factory = [](clang::QualType, llvm::StringRef) {
+          return Lifetime::CreateVariable();
+        };
+        break;
+    }
+
+    Object object = Object::Create(lifetime, var->getType());
+
+    object_repository_.CreateObjects(
+        object, var->getType(), lifetime_factory,
+        /*transitive=*/clang::isa<clang::ParmVarDecl>(var) ||
+            lifetime == Lifetime::Static());
+
+    object_repository_.object_repository_[var] = object;
+    object_repository_.lifetime_value_types_[object] =
+        var->getType()->isArrayType() ? ObjectValueType::kMultiValued
+                                      : ObjectValueType::kSingleValued;
+
+    // Remember the original value of function parameters.
+    if (auto parm_var_decl = clang::dyn_cast<const clang::ParmVarDecl>(var)) {
+      object_repository_.initial_parameter_object_[parm_var_decl] =
+          object_repository_.CloneObject(object);
+    }
+
+    if (var->hasInit() && var->getType()->isRecordType()) {
+      PropagateInitializedObject(var->getInit(), object);
+    }
+  }
+
+  void AddObjectForFunc(clang::FunctionDecl* func) {
+    if (object_repository_.object_repository_.count(func)) {
+      return;
+    }
+
+    object_repository_.object_repository_[func] =
+        Object::CreateFromFunctionDecl(*func);
+  }
+
+  Object AddTemporaryObjectForExpression(clang::Expr* expr) {
+    clang::QualType type = expr->getType().getCanonicalType();
+    Object object = Object::Create(Lifetime::CreateLocal(), type);
+
+    object_repository_.CreateObjects(
+        object, type,
+        [](clang::QualType, llvm::StringRef) {
+          return Lifetime::CreateVariable();
+        },
+        /*transitive=*/false);
+
+    if (type->isRecordType()) {
+      PropagateInitializedObject(expr, object);
+    }
+    return object;
+  }
+
+  // Propagates an `object` of record type that is to be initialized to the
+  // expressions that actually perform the initialization (we call these
+  // "terminating expressions").
+  //
+  // `expr` is the initializer for a variable; this will contain one or
+  // several terminating expressions (such as a CXXConstructExpr, InitListExpr,
+  // or CallExpr).
+  //
+  // Note that not all terminating expressions below `expr` necessarily
+  // initialize `object`; some of these terminating expressions may also
+  // initialize temporary objects. This function takes care to propagate
+  // `object` only to the appropriate terminating expressions.
+  //
+  // The mapping from a terminating expression to the object it initializes
+  // is stored in `object_repository_.initialized_objects_`.
+  void PropagateInitializedObject(const clang::Expr* expr, Object object) {
+    // TODO(danakj): Use StmtVisitor to implement this method.
+    // copybara:begin_strip
+    // Context and hints:
+    // http://cl/414017975/depot/lifetime_analysis/var_decl_objects.cc?version=s3#324
+    // copybara:end_strip
+
+    // Terminating expressions. Expressions that don't initialize a record
+    // object can not be such, and their existence is unexpected as we should
+    // be converting to and initializing a record object from such expressions
+    // further up in the initializer expression's AST. We will assert later in
+    // this function if we find this situation somehow due to incorrect
+    // expectations in this comment.
+    if (IsInitExprInitializingARecordObject(expr)) {
+      if (clang::isa<clang::CXXConstructExpr>(expr) ||
+          clang::isa<clang::CallExpr>(expr) ||
+          clang::isa<clang::ObjCMessageExpr>(expr) ||
+          clang::isa<clang::LambdaExpr>(expr)) {
+        object_repository_.initialized_objects_[expr] = object;
+        return;
+      }
+      if (auto* e = clang::dyn_cast<clang::InitListExpr>(expr)) {
+        if (!e->isSemanticForm()) return;
+        if (e->isTransparent()) {
+          // A field initializer like `S s{cond ? S{} : S{}}` is considered
+          // transparent, and the actual initializer is within.
+          for (const clang::Expr* init : e->inits()) {
+            PropagateInitializedObject(init, object);
+          }
+        } else {
+          object_repository_.initialized_objects_[e] = object;
+        }
+        return;
+      }
+    }
+
+    // Expressions to walk through. Logic is similar to the AggExprEmitter in
+    // clang third_party/llvm-project/clang/lib/CodeGen/CGExprAgg.cpp though we
+    // don't have to visit all the sub-expressions that clang codegen needs to,
+    // as we can stop at terminating expressions and ignore many expressions
+    // that don't occur in the code we're analyzing.
+    if (auto* e = clang::dyn_cast<clang::ParenExpr>(expr)) {
+      PropagateInitializedObject(e->getSubExpr(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::UnaryOperator>(expr)) {
+      PropagateInitializedObject(e->getSubExpr(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::SubstNonTypeTemplateParmExpr>(expr)) {
+      PropagateInitializedObject(e->getReplacement(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::CastExpr>(expr)) {
+      PropagateInitializedObject(e->getSubExpr(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::CXXDefaultArgExpr>(expr)) {
+      PropagateInitializedObject(e->getExpr(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::CXXDefaultInitExpr>(expr)) {
+      PropagateInitializedObject(e->getExpr(), object);
+      return;
+    }
+    if (auto* e = clang::dyn_cast<clang::ExprWithCleanups>(expr)) {
+      PropagateInitializedObject(e->getSubExpr(), object);
+      return;
+    }
+
+    // Expressions that produce a temporary object.
+    if (auto* e = clang::dyn_cast<clang::BinaryOperator>(expr)) {
+      if (e->isCommaOp()) {
+        AddTemporaryObjectForExpression(e->getLHS());
+        PropagateInitializedObject(e->getRHS(), object);
+        return;
+      }
+
+      // Any other binary operator should not produce a record type, it would be
+      // used to construct a record further up the AST, so we should not arrive
+      // here.
+      expr->dump();
+      llvm::report_fatal_error(
+          "Unexpected binary operator in initializer expression tree");
+    }
+    if (auto* e = clang::dyn_cast<clang::AbstractConditionalOperator>(expr)) {
+      AddTemporaryObjectForExpression(e->getCond());
+      PropagateInitializedObject(e->getTrueExpr(), object);
+      PropagateInitializedObject(e->getFalseExpr(), object);
+      return;
+    }
+
+    expr->dump();
+    llvm::report_fatal_error(
+        "Unexpected expression in initializer expression tree");
+  }
+
+  bool InitializedObjectWasPropagatedTo(clang::Expr* terminating_expr) {
+    // An expression that initializes an object should have already been
+    // connected to the object it initializes. We should have walked down from
+    // the object which requires initialization to find its terminating
+    // expressions.
+    if (!object_repository_.initialized_objects_.count(terminating_expr)) {
+      llvm::errs() << "Missing initialized object for terminating expression, "
+                      "we did not record it when we visited something earlier "
+                      "in the tree yet?\n";
+      terminating_expr->dump();
+      return false;
+    } else {
+      return true;
+    }
+  }
+
+  void TraverseCXXMemberInitializers(
+      const clang::CXXConstructorDecl* constructor) {
+    // For constructors, we also need to create lifetimes for variables
+    // referenced by in-class member initializers; the visitor by default only
+    // visits expressions in the initializer list.
+    // We also need to associate member initializers with the members they
+    // initialize.
+    for (const auto* init : constructor->inits()) {
+      const auto* init_expr = init->getInit();
+      if (const auto* default_init =
+              clang::dyn_cast<clang::CXXDefaultInitExpr>(init_expr)) {
+        init_expr = default_init->getExpr();
+      }
+
+      if (init->getMember() && init->getMember()->getType()->isRecordType()) {
+        std::optional<Object> this_object = object_repository_.GetThisObject();
+        assert(this_object.has_value());
+
+        Object field_object =
+            object_repository_.GetFieldObject(*this_object, init->getMember());
+        PropagateInitializedObject(init_expr, field_object);
+      } else if (init->getBaseClass()) {
+        std::optional<Object> this_object = object_repository_.GetThisObject();
+        assert(this_object.has_value());
+
+        Object base_object = object_repository_.GetBaseClassObject(
+            *this_object, init->getBaseClass());
+        PropagateInitializedObject(init_expr, base_object);
+      }
+
+      // Traverse after finishing with the outer expression, including
+      // connecting the initializer (constructor) to its object.
+      TraverseStmt(const_cast<clang::Expr*>(init_expr));
+    }
+  }
+
+  ObjectRepository& object_repository_;
+};
+
+ObjectRepository::ObjectRepository(const clang::FunctionDecl* func) {
+  const auto* method_decl = clang::dyn_cast<clang::CXXMethodDecl>(func);
+
+  const auto* definition = func->getDefinition();
+  assert(definition || (method_decl && method_decl->isPure()));
+  if (definition) func = definition;
+
+  // For the return value, we only need to create field objects.
+  return_object_ =
+      Object::Create(Lifetime::CreateLocal(), func->getReturnType());
+  CreateObjects(
+      return_object_, func->getReturnType(),
+      [](clang::QualType, llvm::StringRef) { return Lifetime::CreateLocal(); },
+      /*transitive=*/false);
+
+  if (method_decl) {
+    if (!method_decl->isStatic()) {
+      this_object_ = Object::Create(Lifetime::CreateVariable(),
+                                    method_decl->getThisObjectType());
+      CreateObjects(
+          *this_object_, method_decl->getThisObjectType(),
+          [](clang::QualType, llvm::StringRef) {
+            return Lifetime::CreateVariable();
+          },
+          /*transitive=*/true);
+    }
+  }
+
+  VarDeclVisitor decl_visitor(*this);
+  if (auto* constructor = clang::dyn_cast<clang::CXXConstructorDecl>(func)) {
+    decl_visitor.TraverseCXXMemberInitializers(constructor);
+  }
+  decl_visitor.TraverseFunctionDecl(const_cast<clang::FunctionDecl*>(func));
+}
+
+std::string ObjectRepository::DebugString() const {
+  std::string result;
+  llvm::raw_string_ostream os(result);
+
+  if (this_object_) {
+    os << "This " << this_object_->DebugString() << "\n";
+  }
+  for (const auto& [decl, object] : object_repository_) {
+    os << decl->getDeclKindName() << " " << decl << " (";
+    decl->printName(os);
+    os << ") object: " << object.DebugString() << "\n";
+  }
+  for (const auto& [expr_i, object] : call_expr_args_objects_) {
+    const auto& [expr, i] = expr_i;
+    os << "Call " << expr << " (arg " << i
+       << ") object: " << object.DebugString() << "\n";
+  }
+  for (const auto& [expr, object] : call_expr_this_pointers_) {
+    os << "Call " << expr << " (this) pointer: " << object.DebugString()
+       << "\n";
+  }
+  os << "InitialPointsToMap:\n" << initial_points_to_map_.DebugString() << "\n";
+  for (const auto& [field, object] : field_object_map_) {
+    os << "Field '";
+    field.second->printName(os);
+    os << "' on " << field.first.Type().getAsString()
+       << " object: " << object.DebugString() << "\n";
+  }
+  os << "Return " << return_object_.DebugString() << "\n";
+  os.flush();
+  return result;
+}
+
+Object ObjectRepository::GetDeclObject(const clang::ValueDecl* decl) const {
+  auto iter = object_repository_.find(decl);
+  if (iter == object_repository_.end()) {
+    llvm::errs() << "Didn't find object for Decl:\n";
+    decl->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find object for Decl");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetTemporaryObject(
+    const clang::MaterializeTemporaryExpr* expr) const {
+  auto iter = temporary_objects_.find(expr);
+  if (iter == temporary_objects_.end()) {
+    llvm::errs() << "Didn't find object for temporary expression:\n";
+    expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find object for temporary expression");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetOriginalParameterValue(
+    const clang::ParmVarDecl* var_decl) const {
+  auto iter = initial_parameter_object_.find(var_decl);
+  if (iter == initial_parameter_object_.end()) {
+    llvm::errs() << "Didn't find caller object for parameter:\n";
+    var_decl->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find caller object for parameter");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetCallExprArgumentObject(const clang::CallExpr* expr,
+                                                   size_t arg_index) const {
+  auto iter = call_expr_args_objects_.find(std::make_pair(expr, arg_index));
+  if (iter == call_expr_args_objects_.end()) {
+    llvm::errs() << "Didn't find object for argument " << arg_index
+                 << " of call:\n";
+    expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find object for argument");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetCallExprThisPointer(
+    const clang::CallExpr* expr) const {
+  auto iter = call_expr_this_pointers_.find(expr);
+  if (iter == call_expr_this_pointers_.end()) {
+    llvm::errs() << "Didn't find `this` object for call:\n";
+    expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find `this` object for call");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetCXXConstructExprArgumentObject(
+    const clang::CXXConstructExpr* expr, size_t arg_index) const {
+  auto iter = call_expr_args_objects_.find(std::make_pair(expr, arg_index));
+  if (iter == call_expr_args_objects_.end()) {
+    llvm::errs() << "Didn't find object for argument " << arg_index
+                 << " of constructor call:\n";
+    expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error(
+        "Didn't find object for argument of constructor call");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetCXXConstructExprThisPointer(
+    const clang::CXXConstructExpr* expr) const {
+  auto iter = call_expr_this_pointers_.find(expr);
+  if (iter == call_expr_this_pointers_.end()) {
+    llvm::errs() << "Didn't find `this` object for constructor:\n";
+    expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find `this` object for constructor");
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetInitializedObject(
+    const clang::Expr* initializer_expr) const {
+  assert(clang::isa<clang::CXXConstructExpr>(initializer_expr) ||
+         clang::isa<clang::InitListExpr>(initializer_expr) ||
+         clang::isa<clang::CallExpr>(initializer_expr));
+
+  auto iter = initialized_objects_.find(initializer_expr);
+  if (iter == initialized_objects_.end()) {
+    llvm::errs() << "Didn't find object for initializer:\n";
+    initializer_expr->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find object for initializer");
+  }
+  return iter->second;
+}
+
+ObjectRepository::ObjectValueType ObjectRepository::GetObjectValueType(
+    Object object) const {
+  auto iter = lifetime_value_types_.find(object);
+  // If we don't know this lifetime, we conservatively assume it to be
+  // multi-valued.
+  if (iter == lifetime_value_types_.end()) {
+    return ObjectValueType::kMultiValued;
+  }
+  return iter->second;
+}
+
+Object ObjectRepository::GetFieldObject(Object struct_object,
+                                        const clang::FieldDecl* field) const {
+  std::optional<Object> field_object =
+      GetFieldObjectInternal(struct_object, field);
+  if (!field_object.has_value()) {
+    llvm::errs() << "On an object of type "
+                 << struct_object.Type().getAsString()
+                 << ", trying to get field:\n";
+    field->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find field object");
+  }
+  return *field_object;
+}
+
+ObjectSet ObjectRepository::GetFieldObject(
+    const ObjectSet& struct_objects, const clang::FieldDecl* field) const {
+  ObjectSet ret;
+  for (Object object : struct_objects) {
+    ret.Add(GetFieldObject(object, field));
+  }
+  return ret;
+}
+
+Object ObjectRepository::GetBaseClassObject(Object struct_object,
+                                            const clang::Type* base) const {
+  base = base->getCanonicalTypeInternal().getTypePtr();
+  auto iter = base_object_map_.find(std::make_pair(struct_object, base));
+  if (iter == base_object_map_.end()) {
+    llvm::errs() << "On object " << struct_object.DebugString()
+                 << ", trying to get base:\n";
+    base->dump();
+    llvm::errs() << "\n" << DebugString();
+    llvm::report_fatal_error("Didn't find base object");
+  }
+  return iter->second;
+}
+
+ObjectSet ObjectRepository::GetBaseClassObject(const ObjectSet& struct_objects,
+                                               const clang::Type* base) const {
+  ObjectSet ret;
+  for (Object object : struct_objects) {
+    ret.Add(GetBaseClassObject(object, base));
+  }
+  return ret;
+}
+
+Object ObjectRepository::CreateStaticObject(clang::QualType type) {
+  auto iter = static_objects_.find(type);
+  if (iter != static_objects_.end()) {
+    return iter->second;
+  }
+
+  Object object = Object::Create(Lifetime::Static(), type);
+  static_objects_[type] = object;
+
+  CreateObjects(
+      object, type,
+      [](clang::QualType, llvm::StringRef) { return Lifetime::Static(); },
+      true);
+
+  return object;
+}
+
+void ObjectRepository::CreateObjects(Object root_object, clang::QualType type,
+                                     LifetimeFactory lifetime_factory,
+                                     bool transitive) {
+  class Visitor : public LifetimeVisitor {
+   public:
+    Visitor(ObjectRepository::FieldObjects& field_object_map,
+            ObjectRepository::BaseObjects& base_object_map,
+            PointsToMap& initial_points_to_map, bool create_transitive_objects)
+        : field_object_map_(field_object_map),
+          base_object_map_(base_object_map),
+          initial_points_to_map_(initial_points_to_map),
+          create_transitive_objects_(create_transitive_objects) {}
+
+    Object GetFieldObject(const ObjectSet& objects,
+                          const clang::FieldDecl* field) override {
+      assert(!objects.empty());
+      std::optional<Object> field_object = std::nullopt;
+
+      for (Object object : objects) {
+        if (auto iter = field_object_map_.find(std::make_pair(object, field));
+            iter != field_object_map_.end()) {
+          field_object = iter->second;
+        }
+      }
+      if (!field_object.has_value()) {
+        field_object =
+            Object::Create((*objects.begin()).GetLifetime(), field->getType());
+      }
+      for (Object object : objects) {
+        field_object_map_[std::make_pair(object, field)] = *field_object;
+      }
+      return *field_object;
+    }
+
+    Object GetBaseClassObject(const ObjectSet& objects,
+                              clang::QualType base) override {
+      assert(!objects.empty());
+      base = base.getCanonicalType();
+      std::optional<Object> base_object = std::nullopt;
+
+      for (Object object : objects) {
+        if (auto iter = base_object_map_.find(std::make_pair(object, &*base));
+            iter != base_object_map_.end()) {
+          base_object = iter->second;
+        }
+      }
+      if (!base_object.has_value()) {
+        base_object = Object::Create((*objects.begin()).GetLifetime(), base);
+      }
+      for (Object object : objects) {
+        base_object_map_[std::make_pair(object, &*base)] = *base_object;
+      }
+      return *base_object;
+    }
+
+    ObjectSet Traverse(const ObjectLifetimes& lifetimes,
+                       const ObjectSet& objects,
+                       int /*pointee_depth*/) override {
+      if (!create_transitive_objects_) return {};
+      if (PointeeType(lifetimes.GetValueLifetimes().Type()).isNull()) {
+        return {};
+      }
+
+      const auto& cache_key =
+          lifetimes.GetValueLifetimes().GetPointeeLifetimes();
+
+      Object child_pointee;
+      if (auto iter = object_cache_.find(cache_key);
+          iter == object_cache_.end()) {
+        child_pointee = Object::Create(
+            lifetimes.GetValueLifetimes().GetPointeeLifetimes().GetLifetime(),
+            PointeeType(lifetimes.GetValueLifetimes().Type()));
+        object_cache_[cache_key] = child_pointee;
+      } else {
+        child_pointee = iter->second;
+      }
+
+      initial_points_to_map_.SetPointerPointsToSet(objects, {child_pointee});
+      return ObjectSet{child_pointee};
+    }
+
+   private:
+    ObjectRepository::FieldObjects& field_object_map_;
+    ObjectRepository::BaseObjects& base_object_map_;
+    PointsToMap& initial_points_to_map_;
+    bool create_transitive_objects_;
+    // Inside of a given VarDecl, we re-use the same Object for all the
+    // sub-objects with the same type and lifetimes. This avoids infinite loops
+    // in the case of structs like lists.
+    llvm::DenseMap<ObjectLifetimes, Object> object_cache_;
+  };
+  Visitor visitor(field_object_map_, base_object_map_, initial_points_to_map_,
+                  transitive);
+  VisitLifetimes(
+      {root_object}, type,
+      ObjectLifetimes(root_object.GetLifetime(),
+                      ValueLifetimes::Create(type, lifetime_factory).get()),
+      visitor);
+}
+
+// Clones an object and its base classes and fields, if any.
+Object ObjectRepository::CloneObject(Object object) {
+  struct ObjectPair {
+    Object orig_object;
+    Object new_object;
+  };
+  auto clone = [this](Object obj) {
+    auto new_obj = Object::Create(obj.GetLifetime(), obj.Type());
+    initial_points_to_map_.SetPointerPointsToSet(
+        new_obj, initial_points_to_map_.GetPointerPointsToSet(obj));
+    return new_obj;
+  };
+  Object new_root = clone(object);
+  std::vector<ObjectPair> object_stack{{object, new_root}};
+  while (!object_stack.empty()) {
+    auto [orig_object, new_object] = object_stack.back();
+    assert(orig_object.Type() == new_object.Type());
+    object_stack.pop_back();
+    auto record_type = orig_object.Type()->getAs<clang::RecordType>();
+    if (!record_type) {
+      continue;
+    }
+
+    // Base classes.
+    if (auto* cxxrecord =
+            clang::dyn_cast<clang::CXXRecordDecl>(record_type->getDecl())) {
+      for (const clang::CXXBaseSpecifier& base : cxxrecord->bases()) {
+        auto base_obj = GetBaseClassObject(orig_object, base.getType());
+        Object new_base_obj = clone(base_obj);
+        base_object_map_[std::make_pair(
+            new_object, base.getType().getCanonicalType().getTypePtr())] =
+            new_base_obj;
+        object_stack.push_back(ObjectPair{base_obj, new_base_obj});
+      }
+    }
+
+    // Fields.
+    for (auto f : record_type->getDecl()->fields()) {
+      auto field_obj = GetFieldObject(orig_object, f);
+      Object new_field_obj = clone(field_obj);
+      field_object_map_[std::make_pair(new_object, f)] = new_field_obj;
+      object_stack.push_back(ObjectPair{field_obj, new_field_obj});
+    }
+  }
+  return new_root;
+}
+
+std::optional<Object> ObjectRepository::GetFieldObjectInternal(
+    Object struct_object, const clang::FieldDecl* field) const {
+  auto iter = field_object_map_.find(std::make_pair(struct_object, field));
+  if (iter != field_object_map_.end()) {
+    return iter->second;
+  }
+  if (auto* cxxrecord = clang::dyn_cast<clang::CXXRecordDecl>(
+          struct_object.Type()->getAs<clang::RecordType>()->getDecl())) {
+    for (const clang::CXXBaseSpecifier& base : cxxrecord->bases()) {
+      std::optional<Object> field_object = GetFieldObjectInternal(
+          GetBaseClassObject(struct_object, base.getType()), field);
+      if (field_object.has_value()) {
+        return field_object;
+      }
+    }
+  }
+  return std::nullopt;
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/object_repository.h b/lifetime_analysis/object_repository.h
new file mode 100644
index 0000000..6a36522
--- /dev/null
+++ b/lifetime_analysis/object_repository.h
@@ -0,0 +1,235 @@
+// 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_OBJECT_REPOSITORY_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_REPOSITORY_H_
+
+#include <functional>
+#include <optional>
+#include <string>
+#include <variant>
+
+#include "lifetime_analysis/object.h"
+#include "lifetime_analysis/object_set.h"
+#include "lifetime_analysis/points_to_map.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "llvm/ADT/DenseMap.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// A record-type expression has 2 modes:
+// 1. If it's being assigned to a reference, then the contents of the expression
+//    are a glvalue. This is because references require an object to point to.
+// 2. If it's being assigned to a record object, then the expression itself is
+//    not creating an object, but initializing it. So the expression's type is
+//    a pure value, and it acts _on_ the initializing object instead of
+//    producing an object.
+inline bool IsInitExprInitializingARecordObject(const clang::Expr* expr) {
+  return expr->getType()->isRecordType() && expr->isPRValue();
+}
+
+// A repository for the objects used in the lifetime analysis of a single
+// function.
+class ObjectRepository {
+ public:
+  // An `Object` might represent objects that have either a single value (such
+  // as plain variables) or multiple ones (such as arrays, or structs).
+  // Assignment behaves differently in the two cases.
+  enum class ObjectValueType {
+    kSingleValued,
+    kMultiValued,
+  };
+
+  // Tag struct for InitializedObject: the object being initialized is the
+  // return value of the function.
+  struct ReturnValue {};
+
+  // Maps a given struct-Object to the Object for each of its fields.
+  // TODO(veluca): this approach does not produce correct results when
+  // diamond-problem-style multiple inheritance happens.
+  using FieldObjects =
+      llvm::DenseMap<std::pair<Object, const clang::FieldDecl*>, Object>;
+
+  // Maps a given struct-Object to the Object for each of its bases.
+  using BaseObjects =
+      llvm::DenseMap<std::pair<Object, const clang::Type*>, Object>;
+
+ private:
+  using MapType = llvm::DenseMap<const clang::ValueDecl*, Object>;
+  // Map from each variable declaration to the object which it declares.
+  MapType object_repository_;
+
+  // Map from each materialized temporary to the object which it declares.
+  llvm::DenseMap<const clang::MaterializeTemporaryExpr*, Object>
+      temporary_objects_;
+
+  // Map from each function parameter to an object representing its initial
+  // value at function entry.
+  llvm::DenseMap<const clang::ParmVarDecl*, Object> initial_parameter_object_;
+
+  // Map from each initializer (constructors or initializer lists) to the object
+  // which it initializes.
+  //
+  // An object in this map may occur in other places too: `object_repository_`
+  // if it is an lvalue, or `return_object_`. Or it may be a temporary in which
+  // case it is only found in this map.
+  llvm::DenseMap<const clang::Expr*, Object> initialized_objects_;
+
+  std::optional<Object> this_object_;
+  Object return_object_;
+
+  llvm::DenseMap<Object, ObjectValueType> lifetime_value_types_;
+
+  class VarDeclVisitor;
+
+  PointsToMap initial_points_to_map_;
+  FieldObjects field_object_map_;
+  BaseObjects base_object_map_;
+
+  llvm::DenseMap<std::pair<const clang::Expr*, size_t>, Object>
+      call_expr_args_objects_;
+
+  llvm::DenseMap<const clang::Expr*, Object> call_expr_this_pointers_;
+
+  llvm::DenseMap<clang::QualType, Object> static_objects_;
+
+ public:
+  using const_iterator = MapType::const_iterator;
+  using value_type = MapType::value_type;
+
+  // Initializes the map with objects for all variables that are declared or
+  // referenced in `func`.
+  explicit ObjectRepository(const clang::FunctionDecl* func);
+
+  // Move-only.
+  ObjectRepository(ObjectRepository&&) = default;
+  ObjectRepository& operator=(ObjectRepository&&) = default;
+
+  // Returns a human-readable representation of the mapping.
+  std::string DebugString() const;
+
+  const_iterator begin() const { return object_repository_.begin(); }
+  const_iterator end() const { return object_repository_.end(); }
+
+  // Returns the object associated with a variable or function.
+  Object GetDeclObject(const clang::ValueDecl* decl) const;
+
+  // Returns the object associated with a materialize temporary expression.
+  Object GetTemporaryObject(const clang::MaterializeTemporaryExpr* expr) const;
+
+  // Returns the object representing the value of a function parameter at
+  // function entry.
+  // Note: This `Object` does not represent the parameter variable itself;
+  // use GetDeclObject() to retrieve that. We're using an `Object` here
+  // because we don't have a dedicated "value" class, but you should not
+  // use this object's identity in any way; i.e. no other `Object` in the
+  // points-to map should ever point to the object returned by this
+  // function.
+  Object GetOriginalParameterValue(const clang::ParmVarDecl* var_decl) const;
+
+  // Returns the object associated with an argument to a CallExpr.
+  Object GetCallExprArgumentObject(const clang::CallExpr* expr,
+                                   size_t arg_index) const;
+
+  // Returns the object associated with the `this` argument to a CallExpr that
+  // represents a method call. Note that this object represents the `this`
+  // pointer, not the object that the method is being called on.
+  Object GetCallExprThisPointer(const clang::CallExpr* expr) const;
+
+  // Returns the object associated with an argument to a CXXConstructExpr.
+  Object GetCXXConstructExprArgumentObject(const clang::CXXConstructExpr* expr,
+                                           size_t arg_index) const;
+
+  // Returns the object associated with the `this` argument to a
+  // CXXConstructExpr. Note that this object represents the `this` pointer, not
+  // the object that the method is being called on (which is represnted by the
+  // object from GetInitializedObject()).
+  Object GetCXXConstructExprThisPointer(
+      const clang::CXXConstructExpr* expr) const;
+
+  // Returns the object associated with, and initialized by, a constructor call
+  // (CXXConstructExpr) or a initializer list (CXXInitListExpr). Note that this
+  // represents the actual class object being initialized, not the `this`
+  // pointer to it that is passed to methods of the class, and which is
+  // represented by the object from GetCXXConstructExprThisPointer().
+  Object GetInitializedObject(const clang::Expr* initializer_expr) const;
+
+  // Returns what kind of values the given object represents.
+  ObjectValueType GetObjectValueType(Object object) const;
+
+  // Returns the object that represents `*this`, if in a member function.
+  std::optional<Object> GetThisObject() const { return this_object_; }
+
+  // Returns the `Object` associated with the return value of the function.
+  // Unlike the `Object`s for variables, the "return value object" is a fiction
+  // -- there is not, in general, going to be a single object associated with
+  // the return value, and it will not, in general, be possible to take the
+  // address of the return value object. It's still a useful fiction, however,
+  // because it allows us to treat return values the same way as other values.
+  Object GetReturnObject() const { return return_object_; }
+
+  // Returns the object associated with a given field in the struct
+  // represented by `struct_object`.
+  Object GetFieldObject(Object struct_object,
+                        const clang::FieldDecl* field) const;
+
+  // Returns the objects associated with a given field in the structs
+  // represented by `struct_objects`.
+  ObjectSet GetFieldObject(const ObjectSet& struct_objects,
+                           const clang::FieldDecl* field) const;
+
+  // Returns FieldObjects; useful for producing debugging output.
+  const FieldObjects& GetFieldObjects() const { return field_object_map_; }
+
+  // Returns the object associated with a given base of the struct
+  // represented by `struct_object`.
+  Object GetBaseClassObject(Object struct_object,
+                            const clang::Type* base) const;
+  Object GetBaseClassObject(Object struct_object,
+                            const clang::QualType base) const {
+    return GetBaseClassObject(struct_object, base.getTypePtr());
+  }
+
+  // Returns the objects associated with a given base of the structs
+  // represented by `struct_object`.
+  ObjectSet GetBaseClassObject(const ObjectSet& struct_objects,
+                               const clang::Type* base) const;
+
+  // Returns BaseObjects; useful for producing debugging output.
+  const BaseObjects& GetBaseObjects() const { return base_object_map_; }
+
+  // Returns the PointsToMap implied by variable declarations, i.e. assuming
+  // that no code has been executed yet.
+  const PointsToMap& InitialPointsToMap() const {
+    return initial_points_to_map_;
+  }
+
+  // Creates and returns an object with static lifetime of the given type.
+  // Also creates any transitive objects if required.
+  // When called multiple times with the same `type`, this function always
+  // returns the same object. This is to guarantee that the number of objects
+  // used in the analysis is bounded and that therefore the lattice is finite
+  // and the analysis terminates.
+  Object CreateStaticObject(clang::QualType type);
+
+ private:
+  void CreateObjects(Object root_object, clang::QualType type,
+                     LifetimeFactory lifetime_factory, bool transitive);
+
+  Object CloneObject(Object object);
+
+  std::optional<Object> GetFieldObjectInternal(
+      Object struct_object, const clang::FieldDecl* field) const;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_REPOSITORY_H_
diff --git a/lifetime_analysis/object_set.cc b/lifetime_analysis/object_set.cc
new file mode 100644
index 0000000..0995492
--- /dev/null
+++ b/lifetime_analysis/object_set.cc
@@ -0,0 +1,27 @@
+// 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/object_set.h"
+
+#include <string>
+#include <vector>
+
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+std::string ObjectSet::DebugString() const {
+  std::vector<std::string> parts;
+  for (Object object : objects_) {
+    parts.push_back(object.DebugString());
+  }
+  return absl::StrJoin(parts, ", ");
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/object_set.h b/lifetime_analysis/object_set.h
new file mode 100644
index 0000000..befe985
--- /dev/null
+++ b/lifetime_analysis/object_set.h
@@ -0,0 +1,98 @@
+// 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_OBJECT_SET_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_SET_H_
+
+#include <initializer_list>
+#include <string>
+
+#include "lifetime_analysis/object.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallSet.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// A set of `Object`s.
+class ObjectSet {
+ public:
+  using const_iterator = llvm::SmallSet<Object, 2>::const_iterator;
+  using value_type = Object;
+
+  ObjectSet() = default;
+
+  ObjectSet(const ObjectSet&) = default;
+  ObjectSet(ObjectSet&&) = default;
+  ObjectSet& operator=(const ObjectSet&) = default;
+  ObjectSet& operator=(ObjectSet&&) = default;
+
+  // Initializes the object set with `objects`.
+  ObjectSet(std::initializer_list<Object> objects) {
+    for (Object object : objects) {
+      objects_.insert(object);
+    }
+  }
+
+  // Returns a human-readable string representation of the object set.
+  std::string DebugString() const;
+
+  const_iterator begin() const { return objects_.begin(); }
+
+  const_iterator end() const { return objects_.end(); }
+
+  bool empty() const { return objects_.empty(); }
+
+  size_t size() const { return objects_.size(); }
+
+  // Returns whether this set contains `object`.
+  bool Contains(Object object) const { return objects_.contains(object); }
+
+  // Returns whether this set contains all objects in `other`, i.e. whether
+  // this set is a superset of `other`.
+  bool Contains(const ObjectSet& other) const {
+    for (Object object : other) {
+      if (!Contains(object)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // Returns a `ObjectSet` containing the union of the pointees from this
+  // `ObjectSet` and `other`.
+  ObjectSet Union(const ObjectSet& other) const {
+    ObjectSet result = *this;
+    result.Add(other);
+    return result;
+  }
+
+  // Adds `object` to this object set.
+  void Add(Object object) { objects_.insert(object); }
+
+  // Adds the `other` objects to this object set.
+  void Add(const ObjectSet& other) {
+    objects_.insert(other.objects_.begin(), other.objects_.end());
+  }
+
+  bool operator==(const ObjectSet& other) const {
+    return objects_ == other.objects_;
+  }
+  bool operator!=(const ObjectSet& other) const { return !(*this == other); }
+
+ private:
+  friend std::ostream& operator<<(std::ostream& os,
+                                  const ObjectSet& object_set) {
+    return os << object_set.DebugString();
+  }
+
+  llvm::SmallSet<Object, 2> objects_;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_OBJECT_SET_H_
diff --git a/lifetime_analysis/object_set_test.cc b/lifetime_analysis/object_set_test.cc
new file mode 100644
index 0000000..0c16f11
--- /dev/null
+++ b/lifetime_analysis/object_set_test.cc
@@ -0,0 +1,134 @@
+// 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/object_set.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_analysis/object.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "lifetime_annotations/test/run_on_code.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+using testing::UnorderedElementsAre;
+
+TEST(ObjectSet, AccessObjects) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object object_static =
+            Object::Create(Lifetime::Static(), ast_context.IntTy);
+        ObjectSet object_set = {object_static};
+
+        EXPECT_THAT(object_set, UnorderedElementsAre(object_static));
+      },
+      {});
+}
+
+TEST(ObjectSet, Contains) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object o1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object o2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains(o1));
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains(o2));
+        EXPECT_FALSE(ObjectSet({o1}).Contains(o2));
+
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains(ObjectSet()));
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains({o1}));
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains({o2}));
+        EXPECT_TRUE(ObjectSet({o1, o2}).Contains({o1, o2}));
+        EXPECT_TRUE(ObjectSet({o1}).Contains({o1}));
+        EXPECT_FALSE(ObjectSet({o1}).Contains({o2}));
+        EXPECT_TRUE(ObjectSet().Contains(ObjectSet()));
+      },
+      {});
+}
+
+TEST(ObjectSet, Union) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object object_static =
+            Object::Create(Lifetime::Static(), ast_context.IntTy);
+        ObjectSet set_1 = {object_static};
+        Object object_local =
+            Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        ObjectSet set_2 = {object_local};
+
+        ObjectSet set_union = set_1.Union(set_2);
+
+        EXPECT_THAT(set_union,
+                    UnorderedElementsAre(object_static, object_local));
+      },
+      {});
+}
+
+TEST(ObjectSet, Add) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object o1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object o2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object o3 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+
+        {
+          ObjectSet object_set = {o1};
+          object_set.Add(o2);
+          EXPECT_THAT(object_set, UnorderedElementsAre(o1, o2));
+        }
+        {
+          ObjectSet object_set = {o1, o2};
+          object_set.Add(o2);
+          EXPECT_THAT(object_set, UnorderedElementsAre(o1, o2));
+        }
+        {
+          ObjectSet object_set = {o1};
+          object_set.Add({o2, o3});
+          EXPECT_THAT(object_set, UnorderedElementsAre(o1, o2, o3));
+        }
+        {
+          ObjectSet object_set = {o1, o2};
+          object_set.Add({o2, o3});
+          EXPECT_THAT(object_set, UnorderedElementsAre(o1, o2, o3));
+        }
+      },
+      {});
+}
+
+TEST(ObjectSet, Equality) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object object_static =
+            Object::Create(Lifetime::Static(), ast_context.IntTy);
+        Object object_local =
+            Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        ObjectSet set_1 = {object_static};
+        ObjectSet set_2 = {object_static};
+        ObjectSet set_3 = {object_static, object_local};
+
+        EXPECT_EQ(set_1, set_2);
+        EXPECT_NE(set_1, set_3);
+      },
+      {});
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/pointer_compatibility.cc b/lifetime_analysis/pointer_compatibility.cc
new file mode 100644
index 0000000..d2001b9
--- /dev/null
+++ b/lifetime_analysis/pointer_compatibility.cc
@@ -0,0 +1,93 @@
+// 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/pointer_compatibility.h"
+
+#include "lifetime_annotations/pointee_type.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// Returns whether `type` is an unsigned integer type or an enum with an
+// underlying unsigned integer type.
+// Note that, unlike this function, Type::isUnsignedIntegerType() considers
+// `bool` to be an unsigned integer type.
+static bool IsUnsignedIntegerOrEnumType(clang::QualType type) {
+  type = type.getCanonicalType();
+  return type->isUnsignedIntegerType() && !type->isBooleanType();
+}
+
+bool PointeesCompatible(clang::QualType pointee_type,
+                        clang::QualType object_type,
+                        clang::ASTContext& ast_context) {
+  assert(!pointee_type.isNull());
+  assert(!object_type.isNull());
+
+  pointee_type = pointee_type.getCanonicalType();
+  object_type = object_type.getCanonicalType();
+
+  // `void *`, `char *`, `unsigned char *` and `std::byte *` are allowed to
+  // point at anything.
+  if (pointee_type->isVoidType() || pointee_type->isCharType() ||
+      pointee_type->isStdByteType()) {
+    return true;
+  }
+
+  // Anything is allowed to point at `void`. IOW, a function is allowed to cast
+  // a void pointer back to any other type of pointer.
+  if (object_type->isVoidType()) {
+    return true;
+  }
+
+  // Records.
+  if (pointee_type->isRecordType()) {
+    const clang::CXXRecordDecl* pointee_record_decl =
+        pointee_type->getAsCXXRecordDecl();
+    const clang::CXXRecordDecl* object_record_decl =
+        object_type->getAsCXXRecordDecl();
+    // We leave the case where the records are the same to the hasSimilarType()
+    // case below.
+    if (pointee_record_decl && object_record_decl &&
+        (object_record_decl->isDerivedFrom(pointee_record_decl) ||
+         pointee_record_decl->isDerivedFrom(object_record_decl))) {
+      return true;
+    }
+  }
+
+  // A signed integer pointer may point to the unsigned variant of the integer
+  // type and vice versa -- so arbitrarily canonicalize integer types to the
+  // signed version.
+  if (IsUnsignedIntegerOrEnumType(pointee_type)) {
+    pointee_type = ast_context.getCorrespondingSignedType(pointee_type);
+  }
+  if (IsUnsignedIntegerOrEnumType(object_type)) {
+    object_type = ast_context.getCorrespondingSignedType(object_type);
+  }
+
+  return ast_context.hasSimilarType(pointee_type, object_type);
+}
+
+bool MayPointTo(clang::QualType pointer_type, clang::QualType object_type,
+                clang::ASTContext& ast_context) {
+  assert(!pointer_type.isNull());
+  assert(!object_type.isNull());
+
+  pointer_type = pointer_type.getCanonicalType();
+  object_type = object_type.getCanonicalType();
+
+  clang::QualType pointee_type = PointeeType(pointer_type);
+
+  if (pointee_type.isNull()) {
+    llvm::report_fatal_error("pointee_type is null");
+  }
+
+  return PointeesCompatible(pointee_type, object_type, ast_context);
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/pointer_compatibility.h b/lifetime_analysis/pointer_compatibility.h
new file mode 100644
index 0000000..6233f5e
--- /dev/null
+++ b/lifetime_analysis/pointer_compatibility.h
@@ -0,0 +1,35 @@
+// 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_POINTER_COMPATIBILITY_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_POINTER_COMPATIBILITY_H_
+
+#include "clang/AST/Type.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// Returns whether a pointer with the given `pointee_type` may point to an
+// object of type `object_type`.
+// In the case where `object_type` is a class type, we also return true if
+// `pointee_type` may point to a type derived from `object_type`. This accounts
+// for the fact that `Object::Type()` may be a base class of the dynamic type
+// of the object instead of being identical to the dynamic type.
+// As described in TransferLifetimesForCall(), this is similar to but more
+// permissive than C++'s strict aliasing rules.
+bool PointeesCompatible(clang::QualType pointee_type,
+                        clang::QualType object_type,
+                        clang::ASTContext& ast_context);
+
+// Returns whether a pointer of the given type may point to an object of type
+// `object_type`.
+bool MayPointTo(clang::QualType pointer_type, clang::QualType object_type,
+                clang::ASTContext& ast_context);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_POINTER_COMPATIBILITY_H_
diff --git a/lifetime_analysis/pointer_compatibility_test.cc b/lifetime_analysis/pointer_compatibility_test.cc
new file mode 100644
index 0000000..f24cefd
--- /dev/null
+++ b/lifetime_analysis/pointer_compatibility_test.cc
@@ -0,0 +1,199 @@
+// 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/pointer_compatibility.h"
+
+#include <functional>
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_annotations/lifetime_annotations.h"
+#include "lifetime_annotations/test/run_on_code.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/Analysis/FlowSensitive/DataflowAnalysis.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+using clang::ast_matchers::cxxRecordDecl;
+using clang::ast_matchers::enumDecl;
+using clang::ast_matchers::hasName;
+using clang::ast_matchers::match;
+using clang::ast_matchers::selectFirst;
+
+clang::QualType getClassType(llvm::StringRef name,
+                             const clang::ASTContext& ast_context) {
+  return ast_context.getRecordType(selectFirst<clang::CXXRecordDecl>(
+      "class", match(cxxRecordDecl(hasName(name)).bind("class"),
+                     const_cast<clang::ASTContext&>(ast_context))));
+}
+
+clang::QualType getEnumType(llvm::StringRef name,
+                            const clang::ASTContext& ast_context) {
+  return ast_context.getEnumType(selectFirst<clang::EnumDecl>(
+      "enum", match(enumDecl(hasName(name)).bind("enum"),
+                    const_cast<clang::ASTContext&>(ast_context))));
+}
+
+bool MayPointTo(clang::QualType pointer_type, clang::QualType object_type,
+                const clang::ASTContext& ast_context) {
+  return clang::tidy::lifetimes::MayPointTo(
+      pointer_type, object_type, const_cast<clang::ASTContext&>(ast_context));
+}
+
+TEST(PointerCompatibilityTest, MayPointTo) {
+  runOnCodeWithLifetimeHandlers(
+      "class Base {};"
+      "class Derived : public Base {};"
+      "class Unrelated {};"
+      "enum SignedEnum {};"
+      "enum UnsignedEnum : unsigned {};"
+      "enum LongEnum : long {};",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        auto pointer_to = [&ast_context](clang::QualType type) {
+          return ast_context.getPointerType(type);
+        };
+
+        clang::QualType void_type = ast_context.VoidTy;
+        clang::QualType char_type = ast_context.CharTy;
+        clang::QualType signed_char_type = ast_context.SignedCharTy;
+        clang::QualType unsigned_char_type = ast_context.UnsignedCharTy;
+        clang::QualType int_type = ast_context.IntTy;
+        clang::QualType unsigned_int_type = ast_context.UnsignedIntTy;
+        clang::QualType long_type = ast_context.LongTy;
+        clang::QualType bool_type = ast_context.BoolTy;
+
+        clang::QualType base_type = getClassType("Base", ast_context);
+        clang::QualType derived_type = getClassType("Derived", ast_context);
+        clang::QualType unrelated_type = getClassType("Unrelated", ast_context);
+        clang::QualType signed_enum_type =
+            getEnumType("SignedEnum", ast_context);
+        clang::QualType unsigned_enum_type =
+            getEnumType("UnsignedEnum", ast_context);
+        clang::QualType long_enum_type = getEnumType("LongEnum", ast_context);
+
+        // Trivial case: A pointer can point to its exact pointee type.
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type), base_type, ast_context));
+
+        // void pointers and character pointers may point to anything.
+        EXPECT_TRUE(MayPointTo(pointer_to(void_type), base_type, ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(char_type), base_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(signed_char_type), base_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(unsigned_char_type), base_type, ast_context));
+
+        // But an int pointer may not point at an unrelated type.
+        EXPECT_FALSE(MayPointTo(pointer_to(int_type), base_type, ast_context));
+
+        // We also allow a void pointer to be converted back to any other
+        // pointer type, but we don't allow the same for character pointers.
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type), void_type, ast_context));
+        EXPECT_FALSE(MayPointTo(pointer_to(base_type), char_type, ast_context));
+        EXPECT_FALSE(
+            MayPointTo(pointer_to(base_type), signed_char_type, ast_context));
+        EXPECT_FALSE(
+            MayPointTo(pointer_to(base_type), unsigned_char_type, ast_context));
+
+        // A signed integer pointer may point to the unsigned variant of the
+        // integer type and vice versa.
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(int_type), unsigned_int_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(unsigned_int_type), int_type, ast_context));
+
+        // An enum pointer may point to any enum that has the same underlying
+        // type or to its underlying type (ignoring signedness in both cases).
+        // Signed enum:
+        EXPECT_TRUE(MayPointTo(pointer_to(signed_enum_type), signed_enum_type,
+                               ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(signed_enum_type), unsigned_enum_type,
+                               ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(signed_enum_type), int_type, ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(signed_enum_type), unsigned_int_type,
+                               ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(int_type), signed_enum_type, ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(unsigned_int_type), signed_enum_type,
+                               ast_context));
+        // Unsigned enum:
+        EXPECT_TRUE(MayPointTo(pointer_to(unsigned_enum_type), signed_enum_type,
+                               ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(unsigned_enum_type),
+                               unsigned_enum_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(unsigned_enum_type), int_type, ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(unsigned_enum_type),
+                               unsigned_int_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(int_type), unsigned_enum_type, ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(unsigned_int_type),
+                               unsigned_enum_type, ast_context));
+        // Underlying types of different width are not compatible:
+        EXPECT_FALSE(MayPointTo(pointer_to(long_enum_type), signed_enum_type,
+                                ast_context));
+        EXPECT_FALSE(
+            MayPointTo(pointer_to(long_type), signed_enum_type, ast_context));
+
+        // A bool pointer may point to bool. This is a regression test for an
+        // assertion failure that we were getting because
+        // Type::isUnsignedIntegerType() considers `bool` to be an unsigned
+        // integer type.
+        EXPECT_TRUE(MayPointTo(pointer_to(bool_type), bool_type, ast_context));
+
+        // But an integer pointer may not point at an integer of a different
+        // size.
+        EXPECT_FALSE(MayPointTo(pointer_to(long_type), int_type, ast_context));
+        EXPECT_FALSE(MayPointTo(pointer_to(int_type), long_type, ast_context));
+
+        // A pointer to a base class may point to an object of the derived
+        // class, and vice versa. However, a pointer to a class type may not
+        // point to an object of a class unrelated by inheritance.
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(base_type), derived_type, ast_context));
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(derived_type), base_type, ast_context));
+        EXPECT_FALSE(
+            MayPointTo(pointer_to(base_type), unrelated_type, ast_context));
+
+        // A pointer to const may point at a non-const object (unsurprisingly),
+        // but we also allow the opposite. IOW, in propagating pointees through
+        // a function, we assume the function may cast away const.
+        // As a side note, this is consistent with the "strict aliasing" rules,
+        // which the pointer's pointee type and the dynamic type of the object
+        // to be similar by C++'s definition of similar.
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type.withConst()), base_type,
+                               ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type), base_type.withConst(),
+                               ast_context));
+
+        // Likewise, a pointer to volatile may point at a non-volatile object
+        // and vice versa.
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type.withVolatile()), base_type,
+                               ast_context));
+        EXPECT_TRUE(MayPointTo(pointer_to(base_type), base_type.withVolatile(),
+                               ast_context));
+
+        // We also allow points-to relationships that would be disallowed by
+        // invariance. The example below is equivalent to the following:
+        // int **pp;
+        // const int ***ppp = &pp;
+        // The code above doesn't compile, but the strict aliasing rules permit
+        // this type of aliasing.
+        EXPECT_TRUE(
+            MayPointTo(pointer_to(pointer_to(pointer_to(int_type.withConst()))),
+                       pointer_to(pointer_to(int_type)), ast_context));
+      },
+      {});
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/points_to_map.cc b/lifetime_analysis/points_to_map.cc
new file mode 100644
index 0000000..bfdeb67
--- /dev/null
+++ b/lifetime_analysis/points_to_map.cc
@@ -0,0 +1,130 @@
+// 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/points_to_map.h"
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "clang/AST/Expr.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+bool PointsToMap::operator==(const PointsToMap& other) const {
+  return pointer_points_tos_ == other.pointer_points_tos_ &&
+         expr_objects_ == other.expr_objects_;
+}
+
+std::string PointsToMap::DebugString() const {
+  std::vector<std::string> parts;
+  for (const auto& [pointer, points_to] : pointer_points_tos_) {
+    parts.push_back(absl::StrFormat("%s -> %s", pointer.DebugString(),
+                                    points_to.DebugString()));
+  }
+  for (const auto& [expr, objects] : expr_objects_) {
+    parts.push_back(absl::StrFormat("%s (%p) -> %s", expr->getStmtClassName(),
+                                    expr, objects.DebugString()));
+  }
+  return absl::StrJoin(parts, "\n");
+}
+
+PointsToMap PointsToMap::Union(const PointsToMap& other) const {
+  PointsToMap result;
+
+  result.pointer_points_tos_ = pointer_points_tos_;
+  for (const auto& [pointer, points_to] : other.pointer_points_tos_) {
+    result.pointer_points_tos_[pointer].Add(points_to);
+  }
+  // TODO(mboehme): Do we even need to perform a union on expression object
+  // sets?
+  result.expr_objects_ = expr_objects_;
+  for (const auto& [expr, objects] : other.expr_objects_) {
+    result.expr_objects_[expr].Add(objects);
+  }
+
+  return result;
+}
+
+ObjectSet PointsToMap::GetPointerPointsToSet(Object pointer) const {
+  auto iter = pointer_points_tos_.find(pointer);
+  if (iter == pointer_points_tos_.end()) {
+    return ObjectSet();
+  }
+  return iter->second;
+}
+
+void PointsToMap::SetPointerPointsToSet(Object pointer, ObjectSet points_to) {
+  pointer_points_tos_[pointer] = std::move(points_to);
+}
+
+void PointsToMap::SetPointerPointsToSet(const ObjectSet& pointers,
+                                        const ObjectSet& points_to) {
+  for (Object pointer : pointers) {
+    SetPointerPointsToSet(pointer, points_to);
+  }
+}
+
+void PointsToMap::ExtendPointerPointsToSet(Object pointer,
+                                           const ObjectSet& points_to) {
+  ObjectSet& set = pointer_points_tos_[pointer];
+  set.Add(points_to);
+}
+
+ObjectSet PointsToMap::GetPointerPointsToSet(const ObjectSet& pointers) const {
+  ObjectSet result;
+  for (Object pointer : pointers) {
+    auto iter = pointer_points_tos_.find(pointer);
+    if (iter != pointer_points_tos_.end()) {
+      result.Add(iter->second);
+    }
+  }
+  return result;
+}
+
+ObjectSet PointsToMap::GetExprObjectSet(const clang::Expr* expr) const {
+  // We can't handle `ParenExpr`s like other `Expr`s because the CFG doesn't
+  // contain `CFGStmt`s for them. Instead, if we encounter a `ParenExpr` here,
+  // we simply return the object set for its subexpression.
+  if (auto paren = clang::dyn_cast<clang::ParenExpr>(expr)) {
+    expr = paren->getSubExpr();
+  }
+
+  assert(expr->isGLValue() || expr->getType()->isPointerType() ||
+         expr->getType()->isArrayType() || expr->getType()->isFunctionType() ||
+         expr->getType()->isBuiltinType());
+
+  auto iter = expr_objects_.find(expr);
+  if (iter == expr_objects_.end()) {
+    llvm::errs() << "Didn't find object set for expression:\n";
+    expr->dump();
+    llvm::report_fatal_error("Didn't find object set for expression");
+  }
+  return iter->second;
+}
+
+void PointsToMap::SetExprObjectSet(const clang::Expr* expr, ObjectSet objects) {
+  assert(expr->isGLValue() || expr->getType()->isPointerType() ||
+         expr->getType()->isArrayType() || expr->getType()->isBuiltinType());
+  expr_objects_[expr] = std::move(objects);
+}
+
+std::vector<Object> PointsToMap::GetAllPointersWithLifetime(
+    Lifetime lifetime) const {
+  std::vector<Object> result;
+  for (const auto& [pointer, _] : pointer_points_tos_) {
+    if (pointer.GetLifetime() == lifetime) {
+      result.push_back(pointer);
+    }
+  }
+  return result;
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/points_to_map.h b/lifetime_analysis/points_to_map.h
new file mode 100644
index 0000000..28e1922
--- /dev/null
+++ b/lifetime_analysis/points_to_map.h
@@ -0,0 +1,104 @@
+// 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_POINTS_TO_MAP_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_POINTS_TO_MAP_H_
+
+#include <string>
+
+#include "lifetime_analysis/object_set.h"
+#include "clang/AST/Expr.h"
+#include "llvm/ADT/DenseMap.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// Maintains the points-to sets needed for the analysis of a function.
+// A `PointsToMap` stores points-to sets for
+// - Objects of reference-like type
+// - Expressions that are prvalues of pointer type or glvalues (glvalues are,
+//   in spirit, references to the object they refer to.)
+// - The function's return value, if it is of reference-like type
+//
+// Note that the relationship between an expression's type and the type of the
+// objects associated with it depends on whether the expression is a glvalue or
+// prvalue:
+// - glvalue expressions are associated with the object that is identified by
+//   the glvalue. This means that the object has the same type as the glvalue
+//   expression.
+// - prvalue expressions of pointer type as are associated with the object that
+//   the pointer points to. This means that if the prvalue expression has type
+//   `T *`, the object has type `T`.
+// The PointsToMap class does not enforce these type relationships because we
+// intend to allow type punning (at least within the implementations of
+// functions).
+class PointsToMap {
+ public:
+  PointsToMap() = default;
+
+  PointsToMap(const PointsToMap&) = default;
+  PointsToMap(PointsToMap&&) = default;
+  PointsToMap& operator=(const PointsToMap&) = default;
+  PointsToMap& operator=(PointsToMap&&) = default;
+
+  bool operator==(const PointsToMap& other) const;
+  bool operator!=(const PointsToMap& other) const { return !(*this == other); }
+
+  // Returns a human-readable representation of this object.
+  std::string DebugString() const;
+
+  const llvm::DenseMap<Object, ObjectSet>& PointerPointsTos() const {
+    return pointer_points_tos_;
+  }
+
+  // Returns a `PointsToMap` containing the union of mappings from this map and
+  // `other`.
+  // If both this map and `other` associate a points-to set with the same
+  // entity, the returned map associates that entity with the union of the
+  // corresponding points-to sets.
+  PointsToMap Union(const PointsToMap& other) const;
+
+  // Returns the points-to set associated with `pointer`, or an empty set if
+  // `pointer` is not associated with a points-to set.
+  ObjectSet GetPointerPointsToSet(Object pointer) const;
+
+  // Associates `pointer` with the given points-to set.
+  void SetPointerPointsToSet(Object pointer, ObjectSet points_to);
+
+  // Associates all `pointers` with the given points-to set.
+  void SetPointerPointsToSet(const ObjectSet& pointers,
+                             const ObjectSet& points_to);
+
+  // Extends a single `pointer`'s points-to set with the given points-to set.
+  void ExtendPointerPointsToSet(Object pointer, const ObjectSet& points_to);
+
+  // Returns the union of the points-to sets associated with the given pointers,
+  // or an empty set if none of the pointers is associated with a points-to set.
+  ObjectSet GetPointerPointsToSet(const ObjectSet& pointers) const;
+
+  // Returns the object set associated with `expr`.
+  // `expr` must previously have been associated with an object set through
+  // a call to SetExprObjectSet(), and the function asserts that this is the
+  // case. We intentionally don't return an empty object set in this case
+  // because we want to notice if we're not propagating object sets through
+  // expressions.
+  ObjectSet GetExprObjectSet(const clang::Expr* expr) const;
+
+  // Associates `expr` with the given object set.
+  void SetExprObjectSet(const clang::Expr* expr, ObjectSet objects);
+
+  // Returns all the pointers (not objects) with the given `lifetime`.
+  std::vector<Object> GetAllPointersWithLifetime(Lifetime lifetime) const;
+
+ private:
+  llvm::DenseMap<Object, ObjectSet> pointer_points_tos_;
+  llvm::DenseMap<const clang::Expr*, ObjectSet> expr_objects_;
+};
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_POINTS_TO_MAP_H_
diff --git a/lifetime_analysis/points_to_map_test.cc b/lifetime_analysis/points_to_map_test.cc
new file mode 100644
index 0000000..912f89f
--- /dev/null
+++ b/lifetime_analysis/points_to_map_test.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
+
+#include "lifetime_analysis/points_to_map.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "lifetime_annotations/lifetime.h"
+#include "lifetime_annotations/test/run_on_code.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+const clang::CallExpr* getFirstCallExpr(const clang::ASTContext& ast_context) {
+  using clang::ast_matchers::callExpr;
+  using clang::ast_matchers::match;
+  using clang::ast_matchers::selectFirst;
+
+  return selectFirst<clang::CallExpr>(
+      "call", match(callExpr().bind("call"),
+                    const_cast<clang::ASTContext&>(ast_context)));
+}
+
+TEST(PointsToMapTest, Equality) {
+  runOnCodeWithLifetimeHandlers(
+      "int *return_int_ptr();"
+      "int* p = return_int_ptr();",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object p1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p3 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        const clang::CallExpr* expr = getFirstCallExpr(ast_context);
+
+        {
+          PointsToMap map1, map2;
+          map1.SetPointerPointsToSet(p1, {p2});
+          map2.SetPointerPointsToSet(p1, {p3});
+          EXPECT_EQ(map1, PointsToMap(map1));
+          EXPECT_NE(map1, PointsToMap());
+          EXPECT_NE(map1, map2);
+        }
+
+        {
+          PointsToMap map1, map2;
+          map1.SetExprObjectSet(expr, {p1});
+          map2.SetExprObjectSet(expr, {p2});
+          EXPECT_EQ(map1, PointsToMap(map1));
+          EXPECT_NE(map1, PointsToMap());
+          EXPECT_NE(map1, map2);
+        }
+      },
+      {});
+}
+
+TEST(PointsToMapTest, Union) {
+  runOnCodeWithLifetimeHandlers(
+      "int *return_int_ptr();"
+      "int* p = return_int_ptr();",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object p1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p3 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        const clang::CallExpr* expr = getFirstCallExpr(ast_context);
+
+        PointsToMap map1, map2;
+        map1.SetPointerPointsToSet(p1, {p2});
+        map2.SetPointerPointsToSet(p1, {p3});
+
+        map1.SetExprObjectSet(expr, {p2});
+        map2.SetExprObjectSet(expr, {p3});
+
+        PointsToMap union_map = map1.Union(map2);
+
+        EXPECT_EQ(union_map.GetPointerPointsToSet(p1), ObjectSet({p2, p3}));
+        EXPECT_EQ(union_map.GetExprObjectSet(expr), ObjectSet({p2, p3}));
+      },
+      {});
+}
+
+TEST(PointsToMapTest, GetPointerPointsToSet) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object p1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p3 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p4 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+
+        PointsToMap map;
+
+        EXPECT_EQ(map.GetPointerPointsToSet(p1), ObjectSet());
+
+        map.SetPointerPointsToSet(p1, {p3});
+        map.SetPointerPointsToSet(p2, {p4});
+
+        EXPECT_EQ(map.GetPointerPointsToSet(p1), ObjectSet({p3}));
+        EXPECT_EQ(map.GetPointerPointsToSet({p1, p2}), ObjectSet({p3, p4}));
+      },
+      {});
+}
+
+TEST(PointsToMapTest, ExtendPointerPointsToSet) {
+  runOnCodeWithLifetimeHandlers(
+      "",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object p1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p2 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        Object p3 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+
+        PointsToMap map;
+
+        EXPECT_EQ(map.GetPointerPointsToSet(p1), ObjectSet());
+
+        map.ExtendPointerPointsToSet(p1, {p2});
+
+        EXPECT_EQ(map.GetPointerPointsToSet(p1), ObjectSet({p2}));
+
+        map.ExtendPointerPointsToSet(p1, {p3});
+
+        EXPECT_EQ(map.GetPointerPointsToSet(p1), ObjectSet({p2, p3}));
+      },
+      {});
+}
+
+TEST(PointsToMapTest, GetExprObjectSet) {
+  runOnCodeWithLifetimeHandlers(
+      "int *return_int_ptr();"
+      "int* p = return_int_ptr();",
+      [](const clang::ASTContext& ast_context,
+         const LifetimeAnnotationContext&) {
+        Object p1 = Object::Create(Lifetime::CreateLocal(), ast_context.IntTy);
+        const clang::CallExpr* expr = getFirstCallExpr(ast_context);
+
+        PointsToMap map;
+
+        map.SetExprObjectSet(expr, {p1});
+        EXPECT_EQ(map.GetExprObjectSet(expr), ObjectSet({p1}));
+      },
+      {});
+}
+
+}  // namespace
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/template_placeholder_support.cc b/lifetime_analysis/template_placeholder_support.cc
new file mode 100644
index 0000000..0daaf7a
--- /dev/null
+++ b/lifetime_analysis/template_placeholder_support.cc
@@ -0,0 +1,226 @@
+// 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/template_placeholder_support.h"
+
+#include <functional>
+#include <memory>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
+#include "absl/strings/str_join.h"
+#include "absl/strings/str_replace.h"
+#include "clang/AST/DeclTemplate.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Analysis/CFG.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/Tooling/Tooling.h"
+#include "clang/Tooling/Transformer/Stencil.h"
+#include "clang/Tooling/Transformer/Transformer.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/VirtualFileSystem.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+namespace {
+
+using clang::ast_matchers::MatchFinder;
+
+class TranslationUnitMatcherCallback : public MatchFinder::MatchCallback {
+ public:
+  explicit TranslationUnitMatcherCallback(
+      std::function<void(clang::ASTContext&)> operation)
+      : operation_{operation} {}
+
+  void run(const MatchFinder::MatchResult& Result) override {
+    const auto* tu = Result.Nodes.getNodeAs<clang::TranslationUnitDecl>("tu");
+    if (!tu) return;
+    operation_(tu->getASTContext());
+  }
+
+  std::function<void(clang::ASTContext&)> operation_;
+};
+
+}  // namespace
+
+llvm::Expected<GeneratedCode> GenerateTemplateInstantiationCode(
+    const clang::TranslationUnitDecl* tu,
+    const llvm::DenseMap<clang::FunctionTemplateDecl*,
+                         const clang::FunctionDecl*>& templates) {
+  using clang::ast_matchers::asString;
+  using clang::ast_matchers::decl;
+  using clang::ast_matchers::equalsNode;
+  using clang::ast_matchers::functionDecl;
+  using clang::ast_matchers::functionTemplateDecl;
+  using clang::ast_matchers::hasBody;
+  using clang::ast_matchers::hasParent;
+  using clang::ast_matchers::loc;
+  using clang::ast_matchers::qualType;
+  using clang::ast_matchers::stmt;
+  using clang::ast_matchers::typeLoc;
+  using clang::tooling::Transformer;
+  using clang::transformer::cat;
+  using clang::transformer::charRange;
+  using clang::transformer::edit;
+  using clang::transformer::EditGenerator;
+  using clang::transformer::name;
+  using clang::transformer::node;
+  using clang::transformer::remove;
+
+  auto& context = tu->getASTContext();
+  auto file_id = tu->getASTContext().getSourceManager().getMainFileID();
+  auto& source_manager = context.getSourceManager();
+  auto source_filename =
+      source_manager.getFilename(source_manager.getLocForStartOfFile(file_id));
+
+  auto source_code = clang::Lexer::getSourceText(
+      clang::CharSourceRange::getTokenRange(
+          source_manager.getLocForStartOfFile(file_id),
+          source_manager.getLocForEndOfFile(file_id)),
+      source_manager, context.getLangOpts());
+
+  llvm::Error err = llvm::Error::success();
+  clang::tooling::AtomicChanges changes;
+  std::vector<std::unique_ptr<Transformer>> transformers;
+
+  auto consumer =
+      [&changes,
+       &err](llvm::Expected<llvm::MutableArrayRef<clang::tooling::AtomicChange>>
+                 c) {
+        if (c) {
+          changes.insert(changes.end(), std::make_move_iterator(c->begin()),
+                         std::make_move_iterator(c->end()));
+        } else {
+          err = c.takeError();
+          llvm::errs() << llvm::toString(c.takeError()) << "\n";
+        }
+      };
+
+  clang::TranslationUnitDecl* translation_unit =
+      context.getTranslationUnitDecl();
+  llvm::DenseSet<const clang::Decl*> toplevels(translation_unit->decls_begin(),
+                                               translation_unit->decls_end());
+
+  int placeholder_suffix_idx = 0;
+  std::vector<std::string> placeholder_classes;
+  for (const auto& [tmpl, func] : templates) {
+    toplevels.erase(tmpl);
+    auto* params = tmpl->getTemplateParameters();
+    std::vector<std::string> parameters;
+    llvm::SmallVector<EditGenerator, 2> edits;
+    std::string func_name = func->getNameAsString();
+
+    for (auto param : *params) {
+      // TODO(kinuko): check the template parameter types, this only assumes
+      // type parameters for now.
+      std::string placeholder_class = absl::StrCat(
+          func_name, "_type_placeholder_", placeholder_suffix_idx++);
+
+      placeholder_classes.push_back(placeholder_class);
+      parameters.push_back(placeholder_class);
+
+      auto change_type_rule =
+          makeRule(typeLoc(loc(qualType(asString(param->getNameAsString())))),
+                   changeTo(cat(placeholder_class)));
+      edits.push_back(rewriteDescendants(func_name, change_type_rule));
+    }
+
+    edits.push_back(edit(changeTo(node("body"), cat(";"))));
+    edits.push_back(edit(
+        changeTo(name(func_name),
+                 cat(absl::StrCat(func->getNameAsString(), "<",
+                                  absl::StrJoin(parameters, ", "), ">")))));
+    edits.push_back(edit(remove(charRange(clang::CharSourceRange::getCharRange(
+        params->getLAngleLoc(), params->getRAngleLoc().getLocWithOffset(1))))));
+
+    auto rule =
+        makeRule(functionDecl(equalsNode(func), hasBody(stmt().bind("body")),
+                              hasParent(functionTemplateDecl()))
+                     .bind(func_name),
+                 flattenVector(edits));
+    transformers.push_back(std::make_unique<Transformer>(rule, consumer));
+  }
+
+  for (const auto* node_to_delete : toplevels) {
+    // Delete all other top-level nodes (we only need the instantiation code as
+    // original code is to be included separately)
+    auto rule = makeRule(decl(equalsNode(node_to_delete)), changeTo(cat("")));
+    transformers.push_back(std::make_unique<Transformer>(rule, consumer));
+  }
+
+  std::string instantiation_code;
+  MatchFinder match_finder;
+  for (const auto& transformer : transformers) {
+    transformer->registerMatchers(&match_finder);
+  }
+  match_finder.matchAST(context);
+
+  // `consumer` might have produced an error.
+  if (err) return std::move(err);
+
+  if ((err = clang::tooling::applyAtomicChanges(
+                 source_filename, source_code, changes,
+                 clang::tooling::ApplyChangesSpec())
+                 .moveInto(instantiation_code))) {
+    return std::move(err);
+  }
+
+  // insertBefore or other transform edits don't work quite well, so simply
+  // concat and add the string.
+  std::vector<std::string> placeholder_definitions;
+  for (auto& c : placeholder_classes) {
+    placeholder_definitions.push_back("struct ");
+    placeholder_definitions.push_back(c);
+    placeholder_definitions.push_back(" {};\n");
+  }
+
+  GeneratedCode generated;
+  generated.filename = (source_filename + "-with-placeholders.cc").str();
+  generated.code = absl::StrCat("#include \"", source_filename.str(), "\"\n",
+                                absl::StrJoin(placeholder_definitions, ""),
+                                instantiation_code);
+  return generated;
+}
+
+void RunToolOnCodeWithOverlay(
+    clang::ASTContext& original_context, const std::string& filename,
+    const std::string& code,
+    const std::function<void(clang::ASTContext&)> operation) {
+  using clang::ast_matchers::MatchFinder;
+  using clang::ast_matchers::translationUnitDecl;
+
+  // Set up an overlay filesystem and add the `code` as a virtual file of it.
+  llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> fs(
+      &original_context.getSourceManager()
+           .getFileManager()
+           .getVirtualFileSystem());
+  auto overlay = llvm::makeIntrusiveRefCnt<llvm::vfs::OverlayFileSystem>(fs);
+  auto memory_fs = llvm::makeIntrusiveRefCnt<llvm::vfs::InMemoryFileSystem>();
+  overlay->pushOverlay(memory_fs);
+  memory_fs->addFile(filename, 0, llvm::MemoryBuffer::getMemBuffer(code));
+
+  clang::ast_matchers::MatchFinder match_finder;
+  TranslationUnitMatcherCallback callback(operation);
+
+  match_finder.addMatcher(translationUnitDecl().bind("tu"), &callback);
+  std::unique_ptr<clang::tooling::FrontendActionFactory> factory(
+      (clang::tooling::newFrontendActionFactory(&match_finder)));
+
+  // TODO(kinuko): get the args from the current ASTContext.
+  clang::tooling::runToolOnCodeWithArgs(factory->create(), code, overlay,
+                                        {"-fsyntax-only", "-std=c++17"},
+                                        filename, "lifetime-with-placedholder");
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/template_placeholder_support.h b/lifetime_analysis/template_placeholder_support.h
new file mode 100644
index 0000000..29f0a85
--- /dev/null
+++ b/lifetime_analysis/template_placeholder_support.h
@@ -0,0 +1,59 @@
+// 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_TEMPLATE_PLACEHOLDER_SUPPORT_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_TEMPLATE_PLACEHOLDER_SUPPORT_H_
+
+#include <functional>
+#include <string>
+
+#include "clang/AST/Decl.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/Support/Error.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+struct GeneratedCode {
+  std::string filename;
+  std::string code;
+};
+
+// Generates a source code that includes the original code for `tu`
+// and also has explicit template instantiation code with placeholder
+// classes for the templates in `templates`.
+// For example, if the main file for the `tu` has the filename
+// "original-file.cc" and looks like the following:
+//
+//    template <typename T>
+//    T* target(T* t) {
+//      return t;
+//    }
+//
+//  This will generate and return the code like the following
+//  (actual generated placeholder classnames will be more cryptic than `T0`):
+//
+//    #include "original-file.cc"
+//    struct T0 {};
+//    template T0* target<T0>(T0* t);
+//
+llvm::Expected<GeneratedCode> GenerateTemplateInstantiationCode(
+    const clang::TranslationUnitDecl* tu,
+    const llvm::DenseMap<clang::FunctionTemplateDecl*,
+                         const clang::FunctionDecl*>& templates);
+
+// Runs the given `operation` on the `code` with `filename`. The `code` is
+// turned into a memory-backed file on a memory filesystem overlaid on top
+// of the original filesystem that's being used by `original_context`.
+void RunToolOnCodeWithOverlay(
+    clang::ASTContext& original_context, const std::string& filename,
+    const std::string& code,
+    const std::function<void(clang::ASTContext&)> operation);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_TEMPLATE_PLACEHOLDER_SUPPORT_H_
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
diff --git a/lifetime_analysis/visit_lifetimes.cc b/lifetime_analysis/visit_lifetimes.cc
new file mode 100644
index 0000000..a334975
--- /dev/null
+++ b/lifetime_analysis/visit_lifetimes.cc
@@ -0,0 +1,172 @@
+// 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/visit_lifetimes.h"
+
+#include <string>
+#include <utility>
+
+#include "lifetime_analysis/object.h"
+#include "lifetime_analysis/object_set.h"
+#include "lifetime_annotations/pointee_type.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Attr.h"
+#include "clang/AST/Attrs.inc"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Type.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+namespace {
+
+llvm::SmallVector<std::string> GetFieldLifetimeArguments(
+    const clang::FieldDecl* field) {
+  // TODO(mboehme): Report errors as Clang diagnostics, not through
+  // llvm::report_fatal_error().
+
+  const clang::AnnotateAttr* member_lifetimes_attr = nullptr;
+  for (auto annotate : field->specific_attrs<clang::AnnotateAttr>()) {
+    if (annotate->getAnnotation() == "member_lifetimes") {
+      if (member_lifetimes_attr) {
+        llvm::report_fatal_error("repeated lifetime annotation");
+      }
+      member_lifetimes_attr = annotate;
+    }
+  }
+  if (!member_lifetimes_attr) {
+    return {};
+  }
+
+  llvm::SmallVector<std::string> ret;
+  for (const auto& arg : member_lifetimes_attr->args()) {
+    llvm::StringRef lifetime;
+    if (llvm::Error err = EvaluateAsStringLiteral(arg, field->getASTContext())
+                              .moveInto(lifetime)) {
+      llvm::report_fatal_error(llvm::StringRef(toString(std::move(err))));
+    }
+    ret.push_back(lifetime.str());
+  }
+
+  return ret;
+}
+
+template <typename Callback>
+void ForEachField(ObjectSet objects, clang::QualType record_type,
+                  const ObjectLifetimes& object_lifetimes,
+                  LifetimeVisitor& visitor, const Callback& callback) {
+  for (clang::FieldDecl* f :
+       record_type->getAs<clang::RecordType>()->getDecl()->fields()) {
+    ObjectLifetimes field_lifetimes = object_lifetimes.GetFieldOrBaseLifetimes(
+        f->getType(), GetFieldLifetimeArguments(f));
+    callback(objects, field_lifetimes, f);
+  }
+  if (auto* cxxrecord = clang::dyn_cast<clang::CXXRecordDecl>(
+          record_type->getAs<clang::RecordType>()->getDecl())) {
+    for (const clang::CXXBaseSpecifier& base : cxxrecord->bases()) {
+      auto base_object_lifetimes = object_lifetimes.GetFieldOrBaseLifetimes(
+          base.getType(), GetLifetimeParameters(base.getType()));
+      auto base_object = visitor.GetBaseClassObject(objects, base.getType());
+      ObjectSet next_objects = objects;
+      next_objects.Add(base_object);
+      ForEachField(next_objects, base.getType(), base_object_lifetimes, visitor,
+                   callback);
+    }
+  }
+}
+
+void VisitLifetimesImpl(const ObjectSet& points_to_set,
+                        const ObjectLifetimes& object_lifetimes,
+                        llvm::DenseSet<Object>& visited_objects,
+                        LifetimeVisitor& visitor, int pointee_depth);
+
+// Traverse fields while walking up base classes. This can be a bit wasteful
+// for cases like diamond inheritance (which is hopefully not common).
+void TraverseObjectFieldsWithBases(const ObjectSet& object_set,
+                                   clang::QualType record_type,
+                                   const ObjectLifetimes& object_lifetimes,
+                                   llvm::DenseSet<Object>& visited_object,
+                                   LifetimeVisitor& visitor,
+                                   int pointee_depth) {
+  assert(record_type->isRecordType());
+  if (record_type->isIncompleteType()) {
+    return;
+  }
+  // Our analysis relies on objects reachable in the same way to be visited in
+  // the same call, thus we need to "merge" together the `Object`s that come
+  // from the same field but different `object`s in the object_set.
+  llvm::SmallVector<std::pair<ObjectSet, ObjectLifetimes>> fields_to_visit;
+  for (Object object : object_set) {
+    // This code relies on the vist order of ForEachField being independent
+    // of `object`.
+    size_t next_field = 0;
+    ForEachField(
+        {object}, record_type, object_lifetimes, visitor,
+        [&](const ObjectSet& bases, const ObjectLifetimes& field_lifetimes,
+            const clang::FieldDecl* f) {
+          size_t field = next_field++;
+          if (field == fields_to_visit.size()) {
+            fields_to_visit.emplace_back(ObjectSet(),
+                                         std::move(field_lifetimes));
+          }
+          Object field_object = visitor.GetFieldObject(bases, f);
+          fields_to_visit[field].first.Add(field_object);
+        });
+  }
+  for (auto [objects, lifetimes] : std::move(fields_to_visit)) {
+    VisitLifetimesImpl(objects, lifetimes, visited_object, visitor,
+                       pointee_depth);
+  }
+}
+
+void VisitLifetimesImpl(const ObjectSet& points_to_set,
+                        const ObjectLifetimes& object_lifetimes,
+                        llvm::DenseSet<Object>& visited_objects,
+                        LifetimeVisitor& visitor, int pointee_depth) {
+  size_t num_visited_before = visited_objects.size();
+  visited_objects.insert(points_to_set.begin(), points_to_set.end());
+  if (num_visited_before == visited_objects.size()) {
+    // No new object -> nothing to do. This avoids infinite loops.
+    return;
+  }
+
+  if (const clang::QualType type = object_lifetimes.GetValueLifetimes().Type();
+      type->isRecordType()) {
+    TraverseObjectFieldsWithBases(points_to_set, type, object_lifetimes,
+                                  visited_objects, visitor, pointee_depth);
+  }
+
+  // TODO(veluca): here we call Traverse even when there is no child type.
+  // This is likely an indication that it is better to split up Traverse into
+  // multiple methods.
+  clang::QualType child_type =
+      PointeeType(object_lifetimes.GetValueLifetimes().Type());
+
+  ObjectSet child_object =
+      visitor.Traverse(object_lifetimes, points_to_set, pointee_depth);
+
+  if (!child_object.empty() && !child_type.isNull()) {
+    VisitLifetimesImpl(
+        child_object,
+        object_lifetimes.GetValueLifetimes().GetPointeeLifetimes(),
+        visited_objects, visitor, pointee_depth + 1);
+  }
+}
+
+}  // namespace
+
+void VisitLifetimes(const ObjectSet& points_to_set, clang::QualType type,
+                    const ObjectLifetimes& object_lifetimes,
+                    LifetimeVisitor& visitor) {
+  llvm::DenseSet<Object> visited_objects;
+  VisitLifetimesImpl(points_to_set, object_lifetimes, visited_objects, visitor,
+                     /*pointee_depth=*/0);
+}
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
diff --git a/lifetime_analysis/visit_lifetimes.h b/lifetime_analysis/visit_lifetimes.h
new file mode 100644
index 0000000..dd9a401
--- /dev/null
+++ b/lifetime_analysis/visit_lifetimes.h
@@ -0,0 +1,64 @@
+// 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_VISIT_LIFETIMES_H_
+#define DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_VISIT_LIFETIMES_H_
+
+#include "lifetime_analysis/object_set.h"
+#include "lifetime_annotations/type_lifetimes.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Type.h"
+#include "llvm/ADT/DenseSet.h"
+
+namespace clang {
+namespace tidy {
+namespace lifetimes {
+
+// A visitor interface used with VisitLifetimes.
+//
+// An implementation of this interface does two things:
+// - Defines how a type with lifetimes should be traversed
+// - Collects information from the traversal
+//
+// An implementation needs to define two functions:
+// - Traverse() maps an object of reference-like type to the corresponding
+//   points-to set. This function typically also collects information from
+//   the traversal.
+// - GetFieldObjects() maps an object of struct type to the objects for
+//   its fields.
+class LifetimeVisitor {
+ public:
+  // Returns the object representing the given `field` of the struct represented
+  // by `objects`. As all the objects in `objects` represent a single class
+  // hierarchy, down to the class that defines the field, they must all have the
+  // same field object.
+  virtual Object GetFieldObject(const ObjectSet& objects,
+                                const clang::FieldDecl* field) = 0;
+  // Returns the object representing the given `base` of the struct represented
+  // by `objects`. As all the objects in `objects` represent a single class
+  // hierarchy, down to the class that defines the base class, they must all
+  // have the same base object.
+  virtual Object GetBaseClassObject(const ObjectSet& objects,
+                                    clang::QualType base) = 0;
+  // Returns the ObjectSet pointed to by the objects in the input
+  // ObjectSet, which are assumed to have lifetimes
+  // `lifetimes`. Returning an empty set will stop the visit.
+  virtual ObjectSet Traverse(const ObjectLifetimes& lifetimes,
+                             const ObjectSet& objects, int pointee_depth) = 0;
+  virtual ~LifetimeVisitor() {}
+};
+
+// Visits the objects and fields of `type` using the given `visitor`;
+// `object_lifetimes` corresponds to the lifetimes of an object of type `type`.
+// `points_to_set` should contain a set of objects that are assumed to be of
+// type `type`.
+void VisitLifetimes(const ObjectSet& points_to_set, clang::QualType type,
+                    const ObjectLifetimes& object_lifetimes,
+                    LifetimeVisitor& visitor);
+
+}  // namespace lifetimes
+}  // namespace tidy
+}  // namespace clang
+
+#endif  // DEVTOOLS_RUST_CC_INTEROP_LIFETIME_ANALYSIS_VISIT_LIFETIMES_H_