aboutsummaryrefslogtreecommitdiff
path: root/clang/unittests/Analysis/LifetimeSafetyTest.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'clang/unittests/Analysis/LifetimeSafetyTest.cpp')
-rw-r--r--clang/unittests/Analysis/LifetimeSafetyTest.cpp710
1 files changed, 710 insertions, 0 deletions
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
new file mode 100644
index 0000000..a48dc45
--- /dev/null
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -0,0 +1,710 @@
+//===- LifetimeSafetyTest.cpp - Lifetime Safety Tests -*---------- C++-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Analysis/Analyses/LifetimeSafety.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Testing/TestAST.h"
+#include "llvm/ADT/StringMap.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <optional>
+#include <vector>
+
+namespace clang::lifetimes::internal {
+namespace {
+
+using namespace ast_matchers;
+using ::testing::UnorderedElementsAreArray;
+
+// A helper class to run the full lifetime analysis on a piece of code
+// and provide an interface for querying the results.
+class LifetimeTestRunner {
+public:
+ LifetimeTestRunner(llvm::StringRef Code) {
+ std::string FullCode = R"(
+ #define POINT(name) void("__lifetime_test_point_" #name)
+ struct MyObj { ~MyObj() {} int i; };
+ )";
+ FullCode += Code.str();
+
+ AST = std::make_unique<clang::TestAST>(FullCode);
+ ASTCtx = &AST->context();
+
+ // Find the target function using AST matchers.
+ auto MatchResult =
+ match(functionDecl(hasName("target")).bind("target"), *ASTCtx);
+ auto *FD = selectFirst<FunctionDecl>("target", MatchResult);
+ if (!FD) {
+ ADD_FAILURE() << "Test case must have a function named 'target'";
+ return;
+ }
+ AnalysisCtx = std::make_unique<AnalysisDeclContext>(nullptr, FD);
+ CFG::BuildOptions &BuildOptions = AnalysisCtx->getCFGBuildOptions();
+ BuildOptions.setAllAlwaysAdd();
+ BuildOptions.AddImplicitDtors = true;
+ BuildOptions.AddTemporaryDtors = true;
+
+ // Run the main analysis.
+ Analysis = std::make_unique<LifetimeSafetyAnalysis>(*AnalysisCtx);
+ Analysis->run();
+
+ AnnotationToPointMap = Analysis->getTestPoints();
+ }
+
+ LifetimeSafetyAnalysis &getAnalysis() { return *Analysis; }
+ ASTContext &getASTContext() { return *ASTCtx; }
+
+ ProgramPoint getProgramPoint(llvm::StringRef Annotation) {
+ auto It = AnnotationToPointMap.find(Annotation);
+ if (It == AnnotationToPointMap.end()) {
+ ADD_FAILURE() << "Annotation '" << Annotation << "' not found.";
+ return nullptr;
+ }
+ return It->second;
+ }
+
+private:
+ std::unique_ptr<TestAST> AST;
+ ASTContext *ASTCtx = nullptr;
+ std::unique_ptr<AnalysisDeclContext> AnalysisCtx;
+ std::unique_ptr<LifetimeSafetyAnalysis> Analysis;
+ llvm::StringMap<ProgramPoint> AnnotationToPointMap;
+};
+
+// A convenience wrapper that uses the LifetimeSafetyAnalysis public API.
+class LifetimeTestHelper {
+public:
+ LifetimeTestHelper(LifetimeTestRunner &Runner)
+ : Runner(Runner), Analysis(Runner.getAnalysis()) {}
+
+ std::optional<OriginID> getOriginForDecl(llvm::StringRef VarName) {
+ auto *VD = findDecl<ValueDecl>(VarName);
+ if (!VD)
+ return std::nullopt;
+ auto OID = Analysis.getOriginIDForDecl(VD);
+ if (!OID)
+ ADD_FAILURE() << "Origin for '" << VarName << "' not found.";
+ return OID;
+ }
+
+ std::optional<LoanID> getLoanForVar(llvm::StringRef VarName) {
+ auto *VD = findDecl<VarDecl>(VarName);
+ if (!VD)
+ return std::nullopt;
+ std::vector<LoanID> LID = Analysis.getLoanIDForVar(VD);
+ if (LID.empty()) {
+ ADD_FAILURE() << "Loan for '" << VarName << "' not found.";
+ return std::nullopt;
+ }
+ // TODO: Support retrieving more than one loans to a var.
+ if (LID.size() > 1) {
+ ADD_FAILURE() << "More than 1 loans found for '" << VarName;
+ return std::nullopt;
+ }
+ return LID[0];
+ }
+
+ std::optional<LoanSet> getLoansAtPoint(OriginID OID,
+ llvm::StringRef Annotation) {
+ ProgramPoint PP = Runner.getProgramPoint(Annotation);
+ if (!PP)
+ return std::nullopt;
+ return Analysis.getLoansAtPoint(OID, PP);
+ }
+
+ std::optional<LoanSet> getExpiredLoansAtPoint(llvm::StringRef Annotation) {
+ ProgramPoint PP = Runner.getProgramPoint(Annotation);
+ if (!PP)
+ return std::nullopt;
+ return Analysis.getExpiredLoansAtPoint(PP);
+ }
+
+private:
+ template <typename DeclT> DeclT *findDecl(llvm::StringRef Name) {
+ auto &Ctx = Runner.getASTContext();
+ auto Results = match(valueDecl(hasName(Name)).bind("v"), Ctx);
+ if (Results.empty()) {
+ ADD_FAILURE() << "Declaration '" << Name << "' not found in AST.";
+ return nullptr;
+ }
+ return const_cast<DeclT *>(selectFirst<DeclT>("v", Results));
+ }
+
+ LifetimeTestRunner &Runner;
+ LifetimeSafetyAnalysis &Analysis;
+};
+
+// ========================================================================= //
+// GTest Matchers & Fixture
+// ========================================================================= //
+
+// A helper class to represent a set of loans, identified by variable names.
+class LoanSetInfo {
+public:
+ LoanSetInfo(const std::vector<std::string> &Vars, LifetimeTestHelper &H)
+ : LoanVars(Vars), Helper(H) {}
+ std::vector<std::string> LoanVars;
+ LifetimeTestHelper &Helper;
+};
+
+// It holds the name of the origin variable and a reference to the helper.
+class OriginInfo {
+public:
+ OriginInfo(llvm::StringRef OriginVar, LifetimeTestHelper &Helper)
+ : OriginVar(OriginVar), Helper(Helper) {}
+ llvm::StringRef OriginVar;
+ LifetimeTestHelper &Helper;
+};
+
+/// Matcher to verify the set of loans held by an origin at a specific
+/// program point.
+///
+/// This matcher is intended to be used with an \c OriginInfo object.
+///
+/// \param LoanVars A vector of strings, where each string is the name of a
+/// variable expected to be the source of a loan.
+/// \param Annotation A string identifying the program point (created with
+/// POINT()) where the check should be performed.
+MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") {
+ const OriginInfo &Info = arg;
+ std::optional<OriginID> OIDOpt = Info.Helper.getOriginForDecl(Info.OriginVar);
+ if (!OIDOpt) {
+ *result_listener << "could not find origin for '" << Info.OriginVar.str()
+ << "'";
+ return false;
+ }
+
+ std::optional<LoanSet> ActualLoansSetOpt =
+ Info.Helper.getLoansAtPoint(*OIDOpt, Annotation);
+ if (!ActualLoansSetOpt) {
+ *result_listener << "could not get a valid loan set at point '"
+ << Annotation << "'";
+ return false;
+ }
+ std::vector<LoanID> ActualLoans(ActualLoansSetOpt->begin(),
+ ActualLoansSetOpt->end());
+
+ std::vector<LoanID> ExpectedLoans;
+ for (const auto &LoanVar : LoanVars) {
+ std::optional<LoanID> ExpectedLIDOpt = Info.Helper.getLoanForVar(LoanVar);
+ if (!ExpectedLIDOpt) {
+ *result_listener << "could not find loan for var '" << LoanVar << "'";
+ return false;
+ }
+ ExpectedLoans.push_back(*ExpectedLIDOpt);
+ }
+
+ return ExplainMatchResult(UnorderedElementsAreArray(ExpectedLoans),
+ ActualLoans, result_listener);
+}
+
+/// Matcher to verify that the complete set of expired loans at a program point
+/// matches the expected loan set.
+MATCHER_P(AreExpiredAt, Annotation, "") {
+ const LoanSetInfo &Info = arg;
+ auto &Helper = Info.Helper;
+
+ auto ActualExpiredSetOpt = Helper.getExpiredLoansAtPoint(Annotation);
+ if (!ActualExpiredSetOpt) {
+ *result_listener << "could not get a valid expired loan set at point '"
+ << Annotation << "'";
+ return false;
+ }
+ std::vector<LoanID> ActualExpiredLoans(ActualExpiredSetOpt->begin(),
+ ActualExpiredSetOpt->end());
+ std::vector<LoanID> ExpectedExpiredLoans;
+ for (const auto &VarName : Info.LoanVars) {
+ auto LoanIDOpt = Helper.getLoanForVar(VarName);
+ if (!LoanIDOpt) {
+ *result_listener << "could not find a loan for variable '" << VarName
+ << "'";
+ return false;
+ }
+ ExpectedExpiredLoans.push_back(*LoanIDOpt);
+ }
+ return ExplainMatchResult(UnorderedElementsAreArray(ExpectedExpiredLoans),
+ ActualExpiredLoans, result_listener);
+}
+
+// Base test fixture to manage the runner and helper.
+class LifetimeAnalysisTest : public ::testing::Test {
+protected:
+ void SetupTest(llvm::StringRef Code) {
+ Runner = std::make_unique<LifetimeTestRunner>(Code);
+ Helper = std::make_unique<LifetimeTestHelper>(*Runner);
+ }
+
+ OriginInfo Origin(llvm::StringRef OriginVar) {
+ return OriginInfo(OriginVar, *Helper);
+ }
+
+ /// Factory function that hides the std::vector creation.
+ LoanSetInfo LoansTo(std::initializer_list<std::string> LoanVars) {
+ return LoanSetInfo({LoanVars}, *Helper);
+ }
+
+ /// A convenience helper for asserting that no loans are expired.
+ LoanSetInfo NoLoans() { return LoansTo({}); }
+
+ // Factory function that hides the std::vector creation.
+ auto HasLoansTo(std::initializer_list<std::string> LoanVars,
+ const char *Annotation) {
+ return HasLoansToImpl(std::vector<std::string>(LoanVars), Annotation);
+ }
+
+ std::unique_ptr<LifetimeTestRunner> Runner;
+ std::unique_ptr<LifetimeTestHelper> Helper;
+};
+
+// ========================================================================= //
+// TESTS
+// ========================================================================= //
+
+TEST_F(LifetimeAnalysisTest, SimpleLoanAndOrigin) {
+ SetupTest(R"(
+ void target() {
+ int x;
+ int* p = &x;
+ POINT(p1);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"x"}, "p1"));
+}
+
+TEST_F(LifetimeAnalysisTest, OverwriteOrigin) {
+ SetupTest(R"(
+ void target() {
+ MyObj s1, s2;
+
+ MyObj* p = &s1;
+ POINT(after_s1);
+
+ p = &s2;
+ POINT(after_s2);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_s1"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_s2"));
+}
+
+TEST_F(LifetimeAnalysisTest, ConditionalLoan) {
+ SetupTest(R"(
+ void target(bool cond) {
+ int a, b;
+ int *p = nullptr;
+ if (cond) {
+ p = &a;
+ POINT(after_then);
+ } else {
+ p = &b;
+ POINT(after_else);
+ }
+ POINT(after_if);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"a"}, "after_then"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"b"}, "after_else"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"a", "b"}, "after_if"));
+}
+
+TEST_F(LifetimeAnalysisTest, PointerChain) {
+ SetupTest(R"(
+ void target() {
+ MyObj y;
+ MyObj* ptr1 = &y;
+ POINT(p1);
+
+ MyObj* ptr2 = ptr1;
+ POINT(p2);
+
+ ptr2 = ptr1;
+ POINT(p3);
+
+ ptr2 = ptr2; // Self assignment
+ POINT(p4);
+ }
+ )");
+ EXPECT_THAT(Origin("ptr1"), HasLoansTo({"y"}, "p1"));
+ EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p2"));
+ EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p3"));
+ EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p4"));
+}
+
+TEST_F(LifetimeAnalysisTest, ReassignToNull) {
+ SetupTest(R"(
+ void target() {
+ MyObj s1;
+ MyObj* p = &s1;
+ POINT(before_null);
+ p = nullptr;
+ POINT(after_null);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_null"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({}, "after_null"));
+}
+
+TEST_F(LifetimeAnalysisTest, ReassignInIf) {
+ SetupTest(R"(
+ void target(bool condition) {
+ MyObj s1, s2;
+ MyObj* p = &s1;
+ POINT(before_if);
+ if (condition) {
+ p = &s2;
+ POINT(after_reassign);
+ }
+ POINT(after_if);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_if"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_reassign"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_if"));
+}
+
+TEST_F(LifetimeAnalysisTest, AssignInSwitch) {
+ SetupTest(R"(
+ void target(int mode) {
+ MyObj s1, s2, s3;
+ MyObj* p = nullptr;
+ switch (mode) {
+ case 1:
+ p = &s1;
+ POINT(case1);
+ break;
+ case 2:
+ p = &s2;
+ POINT(case2);
+ break;
+ default:
+ p = &s3;
+ POINT(case3);
+ break;
+ }
+ POINT(after_switch);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "case1"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "case2"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s3"}, "case3"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2", "s3"}, "after_switch"));
+}
+
+TEST_F(LifetimeAnalysisTest, LoanInLoop) {
+ SetupTest(R"(
+ void target(bool condition) {
+ MyObj* p = nullptr;
+ while (condition) {
+ POINT(start_loop);
+ MyObj inner;
+ p = &inner;
+ POINT(end_loop);
+ }
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "start_loop"));
+ EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("start_loop"));
+
+ EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "end_loop"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop"));
+
+ EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_loop"));
+ EXPECT_THAT(LoansTo({"inner"}), AreExpiredAt("after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, LoopWithBreak) {
+ SetupTest(R"(
+ void target(int count) {
+ MyObj s1;
+ MyObj s2;
+ MyObj* p = &s1;
+ POINT(before_loop);
+ for (int i = 0; i < count; ++i) {
+ if (i == 5) {
+ p = &s2;
+ POINT(inside_if);
+ break;
+ }
+ POINT(after_if);
+ }
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_loop"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "inside_if"));
+ // At the join point after if, s2 cannot make it to p without the if.
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_if"));
+ // At the join point after the loop, p could hold a loan to s1 (if the loop
+ // completed normally) or to s2 (if the loop was broken).
+ EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, PointersInACycle) {
+ SetupTest(R"(
+ void target(bool condition) {
+ MyObj v1, v2, v3;
+ MyObj *p1 = &v1, *p2 = &v2, *p3 = &v3;
+
+ POINT(before_while);
+ while (condition) {
+ MyObj* temp = p1;
+ p1 = p2;
+ p2 = p3;
+ p3 = temp;
+ }
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while"));
+ EXPECT_THAT(Origin("p3"), HasLoansTo({"v3"}, "before_while"));
+
+ // At the fixed point after the loop, all pointers could point to any of
+ // the three variables.
+ EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
+ EXPECT_THAT(Origin("p3"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
+ EXPECT_THAT(Origin("temp"), HasLoansTo({"v1", "v2", "v3"}, "after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, PointersAndExpirationInACycle) {
+ SetupTest(R"(
+ void target(bool condition) {
+ MyObj v1, v2;
+ MyObj *p1 = &v1, *p2 = &v2;
+
+ POINT(before_while);
+ while (condition) {
+ POINT(in_loop_before_temp);
+ MyObj temp;
+ p1 = &temp;
+ POINT(in_loop_after_temp);
+
+ MyObj* q = p1;
+ p1 = p2;
+ p2 = q;
+ }
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_while"));
+
+ EXPECT_THAT(Origin("p1"),
+ HasLoansTo({"v1", "v2", "temp"}, "in_loop_before_temp"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_before_temp"));
+ EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("in_loop_before_temp"));
+
+ EXPECT_THAT(Origin("p1"), HasLoansTo({"temp"}, "in_loop_after_temp"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "in_loop_after_temp"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("in_loop_after_temp"));
+
+ EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "temp"}, "after_loop"));
+ EXPECT_THAT(Origin("p2"), HasLoansTo({"v2", "temp"}, "after_loop"));
+ EXPECT_THAT(LoansTo({"temp"}), AreExpiredAt("after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, NestedScopes) {
+ SetupTest(R"(
+ void target() {
+ MyObj* p = nullptr;
+ {
+ MyObj outer;
+ p = &outer;
+ POINT(before_inner_scope);
+ {
+ MyObj inner;
+ p = &inner;
+ POINT(inside_inner_scope);
+ } // inner expires
+ POINT(after_inner_scope);
+ } // outer expires
+ }
+ )");
+ EXPECT_THAT(Origin("p"), HasLoansTo({"outer"}, "before_inner_scope"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "inside_inner_scope"));
+ EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_inner_scope"));
+}
+
+TEST_F(LifetimeAnalysisTest, SimpleExpiry) {
+ SetupTest(R"(
+ void target() {
+ MyObj* p = nullptr;
+ {
+ MyObj s;
+ p = &s;
+ POINT(before_expiry);
+ } // s goes out of scope here
+ POINT(after_expiry);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_expiry"));
+ EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_expiry"));
+}
+
+TEST_F(LifetimeAnalysisTest, NestedExpiry) {
+ SetupTest(R"(
+ void target() {
+ MyObj s1;
+ MyObj* p = &s1;
+ POINT(before_inner);
+ {
+ MyObj s2;
+ p = &s2;
+ POINT(in_inner);
+ } // s2 expires
+ POINT(after_inner);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_inner"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("in_inner"));
+ EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_inner"));
+}
+
+TEST_F(LifetimeAnalysisTest, ConditionalExpiry) {
+ SetupTest(R"(
+ void target(bool cond) {
+ MyObj s1;
+ MyObj* p = &s1;
+ POINT(before_if);
+ if (cond) {
+ MyObj s2;
+ p = &s2;
+ POINT(then_block);
+ } // s2 expires here
+ POINT(after_if);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_if"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("then_block"));
+ EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("after_if"));
+}
+
+TEST_F(LifetimeAnalysisTest, LoopExpiry) {
+ SetupTest(R"(
+ void target() {
+ MyObj *p = nullptr;
+ for (int i = 0; i < 2; ++i) {
+ POINT(start_loop);
+ MyObj s;
+ p = &s;
+ POINT(end_loop);
+ } // s expires here on each iteration
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("start_loop"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("end_loop"));
+ EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, MultipleExpiredLoans) {
+ SetupTest(R"(
+ void target() {
+ MyObj *p1, *p2, *p3;
+ {
+ MyObj s1;
+ p1 = &s1;
+ POINT(p1);
+ } // s1 expires
+ POINT(p2);
+ {
+ MyObj s2;
+ p2 = &s2;
+ MyObj s3;
+ p3 = &s3;
+ POINT(p3);
+ } // s2, s3 expire
+ POINT(p4);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("p1"));
+ EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p2"));
+ EXPECT_THAT(LoansTo({"s1"}), AreExpiredAt("p3"));
+ EXPECT_THAT(LoansTo({"s1", "s2", "s3"}), AreExpiredAt("p4"));
+}
+
+TEST_F(LifetimeAnalysisTest, GotoJumpsOutOfScope) {
+ SetupTest(R"(
+ void target(bool cond) {
+ MyObj *p = nullptr;
+ {
+ MyObj s;
+ p = &s;
+ POINT(before_goto);
+ if (cond) {
+ goto end;
+ }
+ } // `s` expires here on the path that doesn't jump
+ POINT(after_scope);
+ end:
+ POINT(after_goto);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_goto"));
+ EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_scope"));
+ EXPECT_THAT(LoansTo({"s"}), AreExpiredAt("after_goto"));
+}
+
+TEST_F(LifetimeAnalysisTest, ContinueInLoop) {
+ SetupTest(R"(
+ void target(int count) {
+ MyObj *p = nullptr;
+ MyObj outer;
+ p = &outer;
+ POINT(before_loop);
+
+ for (int i = 0; i < count; ++i) {
+ if (i % 2 == 0) {
+ MyObj s_even;
+ p = &s_even;
+ POINT(in_even_iter);
+ continue;
+ }
+ MyObj s_odd;
+ p = &s_odd;
+ POINT(in_odd_iter);
+ }
+ POINT(after_loop);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("before_loop"));
+ EXPECT_THAT(LoansTo({"s_odd"}), AreExpiredAt("in_even_iter"));
+ EXPECT_THAT(LoansTo({"s_even"}), AreExpiredAt("in_odd_iter"));
+ EXPECT_THAT(LoansTo({"s_even", "s_odd"}), AreExpiredAt("after_loop"));
+}
+
+TEST_F(LifetimeAnalysisTest, ReassignedPointerThenOriginalExpires) {
+ SetupTest(R"(
+ void target() {
+ MyObj* p = nullptr;
+ {
+ MyObj s1;
+ p = &s1;
+ POINT(p_has_s1);
+ {
+ MyObj s2;
+ p = &s2;
+ POINT(p_has_s2);
+ }
+ POINT(p_after_s2_expires);
+ } // s1 expires here.
+ POINT(p_after_s1_expires);
+ }
+ )");
+ EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s1"));
+ EXPECT_THAT(NoLoans(), AreExpiredAt("p_has_s2"));
+ EXPECT_THAT(LoansTo({"s2"}), AreExpiredAt("p_after_s2_expires"));
+ EXPECT_THAT(LoansTo({"s1", "s2"}), AreExpiredAt("p_after_s1_expires"));
+}
+
+} // anonymous namespace
+} // namespace clang::lifetimes::internal