//===- 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 #include 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(FullCode); ASTCtx = &AST->context(); // Find the target function using AST matchers. auto MatchResult = match(functionDecl(hasName("target")).bind("target"), *ASTCtx); auto *FD = selectFirst("target", MatchResult); if (!FD) { ADD_FAILURE() << "Test case must have a function named 'target'"; return; } AnalysisCtx = std::make_unique(nullptr, FD); CFG::BuildOptions &BuildOptions = AnalysisCtx->getCFGBuildOptions(); BuildOptions.setAllAlwaysAdd(); BuildOptions.AddImplicitDtors = true; BuildOptions.AddTemporaryDtors = true; // Run the main analysis. Analysis = std::make_unique(*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 AST; ASTContext *ASTCtx = nullptr; std::unique_ptr AnalysisCtx; std::unique_ptr Analysis; llvm::StringMap AnnotationToPointMap; }; // A convenience wrapper that uses the LifetimeSafetyAnalysis public API. class LifetimeTestHelper { public: LifetimeTestHelper(LifetimeTestRunner &Runner) : Runner(Runner), Analysis(Runner.getAnalysis()) {} std::optional getOriginForDecl(llvm::StringRef VarName) { auto *VD = findDecl(VarName); if (!VD) return std::nullopt; auto OID = Analysis.getOriginIDForDecl(VD); if (!OID) ADD_FAILURE() << "Origin for '" << VarName << "' not found."; return OID; } std::optional getLoanForVar(llvm::StringRef VarName) { auto *VD = findDecl(VarName); if (!VD) return std::nullopt; std::vector 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 getLoansAtPoint(OriginID OID, llvm::StringRef Annotation) { ProgramPoint PP = Runner.getProgramPoint(Annotation); if (!PP) return std::nullopt; return Analysis.getLoansAtPoint(OID, PP); } std::optional getExpiredLoansAtPoint(llvm::StringRef Annotation) { ProgramPoint PP = Runner.getProgramPoint(Annotation); if (!PP) return std::nullopt; return Analysis.getExpiredLoansAtPoint(PP); } private: template 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(selectFirst("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 &Vars, LifetimeTestHelper &H) : LoanVars(Vars), Helper(H) {} std::vector 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 OIDOpt = Info.Helper.getOriginForDecl(Info.OriginVar); if (!OIDOpt) { *result_listener << "could not find origin for '" << Info.OriginVar.str() << "'"; return false; } std::optional ActualLoansSetOpt = Info.Helper.getLoansAtPoint(*OIDOpt, Annotation); if (!ActualLoansSetOpt) { *result_listener << "could not get a valid loan set at point '" << Annotation << "'"; return false; } std::vector ActualLoans(ActualLoansSetOpt->begin(), ActualLoansSetOpt->end()); std::vector ExpectedLoans; for (const auto &LoanVar : LoanVars) { std::optional 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 ActualExpiredLoans(ActualExpiredSetOpt->begin(), ActualExpiredSetOpt->end()); std::vector 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(Code); Helper = std::make_unique(*Runner); } OriginInfo Origin(llvm::StringRef OriginVar) { return OriginInfo(OriginVar, *Helper); } /// Factory function that hides the std::vector creation. LoanSetInfo LoansTo(std::initializer_list 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 LoanVars, const char *Annotation) { return HasLoansToImpl(std::vector(LoanVars), Annotation); } std::unique_ptr Runner; std::unique_ptr 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