Skip to content

[LifetimeSafety] Revamp test suite using unittests #149158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: users/usx95/07-16-lifetime-lattice-tracking-per-point
Choose a base branch
from

Conversation

usx95
Copy link
Contributor

@usx95 usx95 commented Jul 16, 2025

Refactor the Lifetime Safety Analysis infrastructure to support unit testing.

  • Created a public API class LifetimeSafetyAnalysis that encapsulates the analysis functionality
  • Added support for test points via a special TestPointFact that can be used to mark specific program points
  • Added unit tests that verify loan propagation in various code patterns

Copy link
Contributor Author

usx95 commented Jul 16, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch 4 times, most recently from b608946 to 299ff30 Compare July 16, 2025 20:49
Copy link

github-actions bot commented Jul 16, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 299ff30 to 0311169 Compare July 16, 2025 20:56
@usx95 usx95 moved this to In Progress in Lifetime Safety in Clang Jul 16, 2025
@usx95 usx95 changed the base branch from main to users/usx95/07-16-lifetime-lattice-tracking-per-point July 16, 2025 22:07
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 0311169 to 55c972e Compare July 16, 2025 22:07
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 55c972e to 8f3ea32 Compare July 16, 2025 22:27
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-lattice-tracking-per-point branch 2 times, most recently from a0b1ef0 to 3e67c98 Compare July 17, 2025 09:48
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 8f3ea32 to 4c32c7a Compare July 17, 2025 09:48
@usx95 usx95 marked this pull request as ready for review July 17, 2025 12:27
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:frontend Language frontend issues, e.g. anything involving "Sema" clang:analysis labels Jul 17, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 17, 2025

@llvm/pr-subscribers-clang-analysis

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

Changes

Refactor the Lifetime Safety Analysis infrastructure to support unit testing.

  • Created a public API class LifetimeSafetyAnalysis that encapsulates the analysis functionality
  • Added support for test points via a special TestPointFact that can be used to mark specific program points
  • Added unit tests that verify loan propagation in various code patterns

Patch is 28.00 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/149158.diff

5 Files Affected:

  • (modified) clang/include/clang/Analysis/Analyses/LifetimeSafety.h (+73-5)
  • (modified) clang/lib/Analysis/LifetimeSafety.cpp (+128-46)
  • (modified) clang/lib/Sema/AnalysisBasedWarnings.cpp (+2-2)
  • (modified) clang/unittests/Analysis/CMakeLists.txt (+1)
  • (added) clang/unittests/Analysis/LifetimeSafetyTest.cpp (+424)
diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h
index 9998702a41cab..ff71147a20f6c 100644
--- a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h
+++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h
@@ -17,14 +17,82 @@
 //===----------------------------------------------------------------------===//
 #ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
 #define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
-#include "clang/AST/DeclBase.h"
 #include "clang/Analysis/AnalysisDeclContext.h"
 #include "clang/Analysis/CFG.h"
-namespace clang {
+#include "llvm/ADT/ImmutableSet.h"
+#include "llvm/ADT/StringMap.h"
+#include <memory>
 
-void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
-                               AnalysisDeclContext &AC);
+namespace clang::lifetimes {
+namespace internal {
+// Forward declarations of internal types.
+class Fact;
+class FactManager;
+class LoanPropagationAnalysis;
+struct LifetimeFactory;
 
-} // namespace clang
+/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
+/// Used for giving ID to loans and origins.
+template <typename Tag> struct ID {
+  uint32_t Value = 0;
+
+  bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
+  bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
+  bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
+  ID<Tag> operator++(int) {
+    ID<Tag> Tmp = *this;
+    ++Value;
+    return Tmp;
+  }
+  void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
+    IDBuilder.AddInteger(Value);
+  }
+};
+
+using LoanID = ID<struct LoanTag>;
+using OriginID = ID<struct OriginTag>;
+
+// Using LLVM's immutable collections is efficient for dataflow analysis
+// as it avoids deep copies during state transitions.
+// TODO(opt): Consider using a bitset to represent the set of loans.
+using LoanSet = llvm::ImmutableSet<LoanID>;
+using OriginSet = llvm::ImmutableSet<OriginID>;
+
+using ProgramPoint = std::pair<const CFGBlock *, const Fact *>;
+
+/// Running the lifetime safety analysis and querying its results. It
+/// encapsulates the various dataflow analyses.
+class LifetimeSafetyAnalysis {
+public:
+  LifetimeSafetyAnalysis(AnalysisDeclContext &AC);
+  ~LifetimeSafetyAnalysis();
+
+  void run();
+
+  /// Returns the set of loans an origin holds at a specific program point.
+  LoanSet getLoansAtPoint(OriginID OID, ProgramPoint PP) const;
+
+  /// Finds the OriginID for a given declaration.
+  /// Returns a null optional if not found.
+  std::optional<OriginID> getOriginIDForDecl(const ValueDecl *D) const;
+
+  /// Finds the LoanID for a loan created on a specific variable.
+  /// Returns a null optional if not found.
+  std::optional<LoanID> getLoanIDForVar(const VarDecl *VD) const;
+
+  llvm::StringMap<ProgramPoint> getTestPoints() const;
+
+private:
+  AnalysisDeclContext &AC;
+  std::unique_ptr<LifetimeFactory> Factory;
+  std::unique_ptr<FactManager> FactMgr;
+  std::unique_ptr<LoanPropagationAnalysis> LoanPropagation;
+};
+} // namespace internal
+
+/// The main entry point for the analysis.
+void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC);
+
+} // namespace clang::lifetimes
 
 #endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
diff --git a/clang/lib/Analysis/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety.cpp
index a95db6d8013bd..ae5002d680000 100644
--- a/clang/lib/Analysis/LifetimeSafety.cpp
+++ b/clang/lib/Analysis/LifetimeSafety.cpp
@@ -24,8 +24,14 @@
 #include "llvm/Support/TimeProfiler.h"
 #include <cstdint>
 
-namespace clang {
+namespace clang::lifetimes {
+namespace internal {
 namespace {
+template <typename Tag>
+inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
+  return OS << ID.Value;
+}
+} // namespace
 
 /// Represents the storage location being borrowed, e.g., a specific stack
 /// variable.
@@ -36,32 +42,6 @@ struct AccessPath {
   AccessPath(const clang::ValueDecl *D) : D(D) {}
 };
 
-/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
-/// Used for giving ID to loans and origins.
-template <typename Tag> struct ID {
-  uint32_t Value = 0;
-
-  bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
-  bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
-  bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
-  ID<Tag> operator++(int) {
-    ID<Tag> Tmp = *this;
-    ++Value;
-    return Tmp;
-  }
-  void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
-    IDBuilder.AddInteger(Value);
-  }
-};
-
-template <typename Tag>
-inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
-  return OS << ID.Value;
-}
-
-using LoanID = ID<struct LoanTag>;
-using OriginID = ID<struct OriginTag>;
-
 /// Information about a single borrow, or "Loan". A loan is created when a
 /// reference or pointer is created.
 struct Loan {
@@ -223,7 +203,9 @@ class Fact {
     /// An origin is propagated from a source to a destination (e.g., p = q).
     AssignOrigin,
     /// An origin escapes the function by flowing into the return value.
-    ReturnOfOrigin
+    ReturnOfOrigin,
+    /// A marker for a specific point in the code, for testing.
+    TestPoint,
   };
 
 private:
@@ -310,6 +292,24 @@ class ReturnOfOriginFact : public Fact {
   }
 };
 
+/// A dummy-fact used to mark a specific point in the code for testing.
+/// It is generated by recognizing a `void("__lifetime_test_point_...")` cast.
+class TestPointFact : public Fact {
+  std::string Annotation;
+
+public:
+  static bool classof(const Fact *F) { return F->getKind() == Kind::TestPoint; }
+
+  explicit TestPointFact(std::string Annotation)
+      : Fact(Kind::TestPoint), Annotation(std::move(Annotation)) {}
+
+  const std::string &getAnnotation() const { return Annotation; }
+
+  void dump(llvm::raw_ostream &OS) const override {
+    OS << "TestPoint (Annotation: \"" << getAnnotation() << "\")\n";
+  }
+};
+
 class FactManager {
 public:
   llvm::ArrayRef<const Fact *> getFacts(const CFGBlock *B) const {
@@ -363,6 +363,7 @@ class FactManager {
 };
 
 class FactGenerator : public ConstStmtVisitor<FactGenerator> {
+  using Base = ConstStmtVisitor<FactGenerator>;
 
 public:
   FactGenerator(FactManager &FactMgr, AnalysisDeclContext &AC)
@@ -458,6 +459,15 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
     }
   }
 
+  void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE) {
+    // Check if this is a test point marker. If so, we are done with this
+    // expression.
+    if (VisitTestPoint(FCE))
+      return;
+    // Visit as normal otherwise.
+    Base::VisitCXXFunctionalCastExpr(FCE);
+  }
+
 private:
   // Check if a type has an origin.
   bool hasOrigin(QualType QT) { return QT->isPointerOrReferenceType(); }
@@ -491,6 +501,27 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
     }
   }
 
+  /// Checks if the expression is a `void("__lifetime_test_point_...")` cast.
+  /// If so, creates a `TestPointFact` and returns true.
+  bool VisitTestPoint(const CXXFunctionalCastExpr *FCE) {
+    if (!FCE->getType()->isVoidType())
+      return false;
+
+    const auto *SubExpr = FCE->getSubExpr()->IgnoreParenImpCasts();
+    if (const auto *SL = dyn_cast<StringLiteral>(SubExpr)) {
+      llvm::StringRef LiteralValue = SL->getString();
+      const std::string Prefix = "__lifetime_test_point_";
+
+      if (LiteralValue.starts_with(Prefix)) {
+        std::string Annotation = LiteralValue.drop_front(Prefix.length()).str();
+        CurrentBlockFacts.push_back(
+            FactMgr.createFact<TestPointFact>(Annotation));
+        return true;
+      }
+    }
+    return false;
+  }
+
   FactManager &FactMgr;
   AnalysisDeclContext &AC;
   llvm::SmallVector<Fact *> CurrentBlockFacts;
@@ -637,6 +668,8 @@ class DataflowAnalysis {
       return D->transfer(In, *F->getAs<AssignOriginFact>());
     case Fact::Kind::ReturnOfOrigin:
       return D->transfer(In, *F->getAs<ReturnOfOriginFact>());
+    case Fact::Kind::TestPoint:
+      return D->transfer(In, *F->getAs<TestPointFact>());
     }
     llvm_unreachable("Unknown fact kind");
   }
@@ -646,14 +679,16 @@ class DataflowAnalysis {
   Lattice transfer(Lattice In, const ExpireFact &) { return In; }
   Lattice transfer(Lattice In, const AssignOriginFact &) { return In; }
   Lattice transfer(Lattice In, const ReturnOfOriginFact &) { return In; }
+  Lattice transfer(Lattice In, const TestPointFact &) { return In; }
 };
 
 namespace utils {
 
 /// Computes the union of two ImmutableSets.
 template <typename T>
-llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
-                           typename llvm::ImmutableSet<T>::Factory &F) {
+static llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A,
+                                  llvm::ImmutableSet<T> B,
+                                  typename llvm::ImmutableSet<T>::Factory &F) {
   if (A.getHeight() < B.getHeight())
     std::swap(A, B);
   for (const T &E : B)
@@ -666,7 +701,7 @@ llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
 // efficient merge could be implemented using a Patricia Trie or HAMT
 // instead of the current AVL-tree-based ImmutableMap.
 template <typename K, typename V, typename Joiner>
-llvm::ImmutableMap<K, V>
+static llvm::ImmutableMap<K, V>
 join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
      typename llvm::ImmutableMap<K, V>::Factory &F, Joiner joinValues) {
   if (A.getHeight() < B.getHeight())
@@ -690,10 +725,6 @@ join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
 //                          Loan Propagation Analysis
 // ========================================================================= //
 
-// Using LLVM's immutable collections is efficient for dataflow analysis
-// as it avoids deep copies during state transitions.
-// TODO(opt): Consider using a bitset to represent the set of loans.
-using LoanSet = llvm::ImmutableSet<LoanID>;
 using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>;
 
 /// An object to hold the factories for immutable collections, ensuring
@@ -807,17 +838,27 @@ class LoanPropagationAnalysis
 // - Modify origin liveness analysis to answer `bool isLive(Origin O, Point P)`
 // - Using the above three to perform the final error reporting.
 // ========================================================================= //
-} // anonymous namespace
 
-void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
-                               AnalysisDeclContext &AC) {
+// ========================================================================= //
+//                  LifetimeSafetyAnalysis Class Implementation
+// ========================================================================= //
+
+LifetimeSafetyAnalysis::~LifetimeSafetyAnalysis() = default;
+
+LifetimeSafetyAnalysis::LifetimeSafetyAnalysis(AnalysisDeclContext &AC)
+    : AC(AC), Factory(std::make_unique<LifetimeFactory>()),
+      FactMgr(std::make_unique<FactManager>()) {}
+
+void LifetimeSafetyAnalysis::run() {
   llvm::TimeTraceScope TimeProfile("LifetimeSafetyAnalysis");
+
+  const CFG &Cfg = *AC.getCFG();
   DEBUG_WITH_TYPE("PrintCFG", Cfg.dump(AC.getASTContext().getLangOpts(),
                                        /*ShowColors=*/true));
-  FactManager FactMgr;
-  FactGenerator FactGen(FactMgr, AC);
+
+  FactGenerator FactGen(*FactMgr, AC);
   FactGen.run();
-  DEBUG_WITH_TYPE("LifetimeFacts", FactMgr.dump(Cfg, AC));
+  DEBUG_WITH_TYPE("LifetimeFacts", FactMgr->dump(Cfg, AC));
 
   /// TODO(opt): Consider optimizing individual blocks before running the
   /// dataflow analysis.
@@ -828,9 +869,50 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
   ///    blocks; only Decls are visible.  Therefore, loans in a block that
   ///    never reach an Origin associated with a Decl can be safely dropped by
   ///    the analysis.
-  LifetimeFactory Factory;
-  LoanPropagationAnalysis LoanPropagation(Cfg, AC, FactMgr, Factory);
-  LoanPropagation.run();
-  DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation.dump());
+  LoanPropagation =
+      std::make_unique<LoanPropagationAnalysis>(Cfg, AC, *FactMgr, *Factory);
+  LoanPropagation->run();
+  DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation->dump());
+}
+
+LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID,
+                                                ProgramPoint PP) const {
+  assert(LoanPropagation && "Analysis has not been run.");
+  return LoanPropagation->getLoans(OID, PP);
+}
+
+std::optional<OriginID>
+LifetimeSafetyAnalysis::getOriginIDForDecl(const ValueDecl *D) const {
+  assert(FactMgr && "FactManager not initialized");
+  // This assumes the OriginManager's `get` can find an existing origin.
+  // We might need a `find` method on OriginManager to avoid `getOrCreate` logic
+  // in a const-query context if that becomes an issue.
+  return FactMgr->getOriginMgr().get(*D);
+}
+
+std::optional<LoanID>
+LifetimeSafetyAnalysis::getLoanIDForVar(const VarDecl *VD) const {
+  assert(FactMgr && "FactManager not initialized");
+  for (const Loan &L : FactMgr->getLoanMgr().getLoans()) {
+    if (L.Path.D == VD)
+      return L.ID;
+  }
+  return std::nullopt;
+}
+
+llvm::StringMap<ProgramPoint> LifetimeSafetyAnalysis::getTestPoints() const {
+  assert(FactMgr && "FactManager not initialized");
+  llvm::StringMap<ProgramPoint> AnnotationToPointMap;
+  for (const CFGBlock *Block : *AC.getCFG())
+    for (const Fact *F : FactMgr->getFacts(Block))
+      if (const auto *TPF = F->getAs<TestPointFact>())
+        AnnotationToPointMap[TPF->getAnnotation()] = {Block, F};
+  return AnnotationToPointMap;
+}
+} // namespace internal
+
+void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC) {
+  internal::LifetimeSafetyAnalysis Analysis(AC);
+  Analysis.run();
 }
-} // namespace clang
+} // namespace clang::lifetimes
diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp
index 5eba024e83634..89c5a3596f584 100644
--- a/clang/lib/Sema/AnalysisBasedWarnings.cpp
+++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp
@@ -3030,8 +3030,8 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings(
   // TODO: Enable lifetime safety analysis for other languages once it is
   // stable.
   if (EnableLifetimeSafetyAnalysis && S.getLangOpts().CPlusPlus) {
-    if (CFG *cfg = AC.getCFG())
-      runLifetimeSafetyAnalysis(*cast<DeclContext>(D), *cfg, AC);
+    if (AC.getCFG())
+      lifetimes::runLifetimeSafetyAnalysis(AC);
   }
   // Check for violations of "called once" parameter properties.
   if (S.getLangOpts().ObjC && !S.getLangOpts().CPlusPlus &&
diff --git a/clang/unittests/Analysis/CMakeLists.txt b/clang/unittests/Analysis/CMakeLists.txt
index 059a74843155c..52e7d2854633d 100644
--- a/clang/unittests/Analysis/CMakeLists.txt
+++ b/clang/unittests/Analysis/CMakeLists.txt
@@ -4,6 +4,7 @@ add_clang_unittest(ClangAnalysisTests
   CloneDetectionTest.cpp
   ExprMutationAnalyzerTest.cpp
   IntervalPartitionTest.cpp
+  LifetimeSafetyTest.cpp
   MacroExpansionContextTest.cpp
   UnsafeBufferUsageTest.cpp
   CLANG_LIBS
diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
new file mode 100644
index 0000000000000..49339f4b99a34
--- /dev/null
+++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp
@@ -0,0 +1,424 @@
+//===- 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();
+
+    TestAST = std::make_unique<clang::TestAST>(FullCode);
+    ASTCtx = &TestAST->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);
+    AnalysisCtx->getCFGBuildOptions().setAllAlwaysAdd();
+
+    // 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, nullptr};
+    }
+    return It->second;
+  }
+
+private:
+  std::unique_ptr<TestAST> TestAST;
+  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;
+    auto LID = Analysis.getLoanIDForVar(VD);
+    if (!LID)
+      ADD_FAILURE() << "Loan for '" << VarName << "' not found.";
+    return LID;
+  }
+
+  std::optional<LoanSet> getLoansAtPoint(OriginID OID,
+                                         llvm::StringRef Annotation) {
+    ProgramPoint PP = Runner.getProgramPoint(Annotation);
+    if (!PP.first)
+      return std::nullopt;
+    return Analysis.getLoansAtPoint(OID, 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
+// ========================================================================= //
+
+// 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;
+};
+
+// The implementation of the matcher. It takes a vector of strings.
+MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") {
+  const OriginInfo &Info = arg;
+  std::optional<OriginID> OIDOpt = Info....
[truncated]

@usx95 usx95 requested review from jvoung, Xazax-hun and ymand July 17, 2025 12:28
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 4c32c7a to 1b945d0 Compare July 17, 2025 12:29
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 16fccbd to 3e0b53f Compare July 17, 2025 18:54
@usx95 usx95 force-pushed the users/usx95/07-16-lifetime-safety-add-unit-tests branch from 3e0b53f to e6fc855 Compare July 18, 2025 11:57
@usx95 usx95 requested a review from Xazax-hun July 18, 2025 12:08
@usx95 usx95 self-assigned this Jul 19, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:analysis clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category
Projects
Status: In Progress
Development

Successfully merging this pull request may close these issues.

3 participants