//===- 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
