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 += "'";
+ break;
+ case '"':
+ escaped += """;
+ break;
+ case '<':
+ escaped += "<";
+ break;
+ case '>':
+ escaped += ">";
+ break;
+ case '&':
+ escaped += "&";
+ 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_