https://github.com/steakhal updated 
https://github.com/llvm/llvm-project/pull/188753

From 209b97ae1b88b51667134f69b3fbe0297cd9b3e0 Mon Sep 17 00:00:00 2001
From: Balazs Benics <[email protected]>
Date: Thu, 26 Mar 2026 13:50:56 +0000
Subject: [PATCH 1/2] [clang][ssaf] Add CallGraph summary and extractor

rdar://170258016
---
 .../Analyses/CallGraph/CallGraphSummary.h     |  51 +++
 .../SSAFBuiltinForceLinker.h                  |   7 +
 clang/lib/Driver/CMakeLists.txt               |   1 +
 clang/lib/FrontendTool/CMakeLists.txt         |   1 +
 .../Analyses/CMakeLists.txt                   |   1 +
 .../Analyses/CallGraph/CallGraphExtractor.cpp | 110 +++++
 clang/tools/clang-ssaf-format/CMakeLists.txt  |   1 +
 clang/tools/clang-ssaf-linker/CMakeLists.txt  |   1 +
 .../CallGraph/CallGraphExtractorTest.cpp      | 375 ++++++++++++++++++
 .../CMakeLists.txt                            |   1 +
 .../SummaryExtractorRegistryTest.cpp          |   8 +-
 .../gn/secondary/clang/lib/Driver/BUILD.gn    |   1 +
 .../secondary/clang/lib/FrontendTool/BUILD.gn |   1 +
 .../Analyses/BUILD.gn                         |   5 +-
 .../ScalableStaticAnalysisFramework/BUILD.gn  |   3 +-
 15 files changed, 560 insertions(+), 7 deletions(-)
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp

diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
new file mode 100644
index 0000000000000..03bac3c6dd8e0
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
@@ -0,0 +1,51 @@
+//===- CallGraphSummary.h ---------------------------------------*- 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
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_ANALYSES_CALLGRAPH_CALLGRAPHSUMMARY_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_ANALYSES_CALLGRAPH_CALLGRAPHSUMMARY_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/EntitySummary.h"
+#include <set>
+
+namespace clang::ssaf {
+
+/// Summary of direct call-graph edges for a single function entity.
+///
+/// Represents a function definition, and information about its callees.
+struct CallGraphSummary final : public EntitySummary {
+  struct Location {
+    std::string File;
+    unsigned Line;
+    unsigned Column;
+  };
+
+  SummaryName getSummaryName() const override {
+    return SummaryName("CallGraph");
+  }
+
+  /// Represents the location of the function.
+  Location Definition = {};
+
+  /// The set of direct callees of this function.
+  std::set<EntityId> DirectCallees;
+
+  /// A human-readable name of the function.
+  /// This is not guaranteed to be accurate or unique.
+  std::string PrettyName;
+
+  /// Whether this function contains calls that could not be resolved to a
+  /// direct callee.
+  /// E.g. virtual method calls, or calls through function pointers.
+  bool HasIndirectCalls = false;
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_ANALYSES_CALLGRAPH_CALLGRAPHSUMMARY_H
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h 
b/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
index 2f144b92a1a94..707573ce34e46 100644
--- 
a/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
@@ -20,6 +20,8 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SSAFBUILTINFORCELINKER_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SSAFBUILTINFORCELINKER_H
 
+// TODO: Move these to the `clang::ssaf` namespace.
+
 // This anchor is used to force the linker to link the JSONFormat registration.
 extern volatile int SSAFJSONFormatAnchorSource;
 [[maybe_unused]] static int SSAFJSONFormatAnchorDestination =
@@ -30,4 +32,9 @@ extern volatile int SSAFAnalysisRegistryAnchorSource;
 [[maybe_unused]] static int SSAFAnalysisRegistryAnchorDestination =
     SSAFAnalysisRegistryAnchorSource;
 
+// This anchor is used to force the linker to link the CallGraphExtractor.
+extern volatile int CallGraphExtractorAnchorSource;
+[[maybe_unused]] static int CallGraphExtractorAnchorDestination =
+    CallGraphExtractorAnchorSource;
+
 #endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SSAFBUILTINFORCELINKER_H
diff --git a/clang/lib/Driver/CMakeLists.txt b/clang/lib/Driver/CMakeLists.txt
index 8a2ffb303b4cb..92554712fffd8 100644
--- a/clang/lib/Driver/CMakeLists.txt
+++ b/clang/lib/Driver/CMakeLists.txt
@@ -110,6 +110,7 @@ add_clang_library(clangDriver
   clangBasic
   clangDependencyScanning
   clangFrontend
+  clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkFrontend
   clangSerialization
diff --git a/clang/lib/FrontendTool/CMakeLists.txt 
b/clang/lib/FrontendTool/CMakeLists.txt
index a451eb967e904..24623303e6bdb 100644
--- a/clang/lib/FrontendTool/CMakeLists.txt
+++ b/clang/lib/FrontendTool/CMakeLists.txt
@@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 set(link_libs
+  clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkFrontend
   clangBasic
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Analyses/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CMakeLists.txt
index 34c6dd9b61203..2dcce40f886dd 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Analyses/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CMakeLists.txt
@@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangScalableStaticAnalysisFrameworkAnalyses
+  CallGraph/CallGraphExtractor.cpp
   UnsafeBufferUsage/UnsafeBufferUsageExtractor.cpp
 
   LINK_LIBS
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
new file mode 100644
index 0000000000000..09b237ccf8e3d
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
@@ -0,0 +1,110 @@
+//===- CallGraphExtractor.cpp - Call Graph Summary Extractor 
--------------===//
+//
+// 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/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DeclObjC.h"
+#include "clang/Analysis/AnalysisDeclContext.h"
+#include "clang/Analysis/CallGraph.h"
+#include "clang/Basic/SourceManager.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/ASTEntityMapping.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/TUSummary/ExtractorRegistry.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummaryBuilder.h"
+#include "llvm/ADT/STLExtras.h"
+#include <memory>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+class CallGraphExtractor final : public TUSummaryExtractor {
+public:
+  using TUSummaryExtractor::TUSummaryExtractor;
+
+private:
+  void HandleTranslationUnit(ASTContext &Ctx) override;
+
+  void handleCallGraphNode(const ASTContext &Ctx, const CallGraphNode *N);
+};
+} // namespace
+
+void CallGraphExtractor::HandleTranslationUnit(ASTContext &Ctx) {
+  CallGraph CG;
+  CG.addToCallGraph(
+      const_cast<TranslationUnitDecl *>(Ctx.getTranslationUnitDecl()));
+
+  for (const auto &N : llvm::make_second_range(CG)) {
+    if (N && N->getDecl() && N->getDefinition())
+      handleCallGraphNode(Ctx, N.get());
+  }
+}
+
+void CallGraphExtractor::handleCallGraphNode(const ASTContext &Ctx,
+                                             const CallGraphNode *N) {
+  const FunctionDecl *Definition = N->getDefinition();
+
+  // Ignore templates for now.
+  if (Definition->isTemplated())
+    return;
+
+  auto CallerName = getEntityName(Definition);
+  if (!CallerName)
+    return;
+
+  auto FnSummary = std::make_unique<CallGraphSummary>();
+
+  PresumedLoc Loc =
+      Ctx.getSourceManager().getPresumedLoc(Definition->getLocation());
+  FnSummary->Definition.File = Loc.getFilename();
+  FnSummary->Definition.Line = Loc.getLine();
+  FnSummary->Definition.Column = Loc.getColumn();
+  FnSummary->PrettyName = AnalysisDeclContext::getFunctionName(Definition);
+
+  for (const auto &Record : N->callees()) {
+    const Decl *CalleeDecl = Record.Callee->getDecl();
+    if (!CalleeDecl) {
+      FnSummary->HasIndirectCalls = true;
+      continue;
+    }
+    assert(!CalleeDecl->isTemplated());
+
+    // Objective-C methods might be replaced at runtime, so they are 
effectively
+    // indirect calls.
+    if (isa<ObjCMethodDecl>(CalleeDecl)) {
+      FnSummary->HasIndirectCalls = true;
+      continue;
+    }
+
+    // Treat virtual functions as indirect calls for now.
+    if (const auto *MD = dyn_cast_or_null<CXXMethodDecl>(CalleeDecl);
+        MD && MD->isVirtual()) {
+      FnSummary->HasIndirectCalls = true;
+      continue;
+    }
+
+    auto CalleeName = getEntityName(CalleeDecl);
+    if (!CalleeName)
+      continue;
+
+    EntityId CalleeId = SummaryBuilder.addEntity(*CalleeName);
+    FnSummary->DirectCallees.insert(CalleeId);
+  }
+
+  EntityId CallerId = SummaryBuilder.addEntity(*CallerName);
+  SummaryBuilder.addSummary(CallerId, std::move(FnSummary));
+}
+
+static TUSummaryExtractorRegistry::Add<CallGraphExtractor>
+    RegisterExtractor("CallGraph", "Extracts static call-graph information");
+
+// This anchor is used to force the linker to link in the generated object file
+// and thus register the CallGraphExtractor.
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int CallGraphExtractorAnchorSource = 0;
diff --git a/clang/tools/clang-ssaf-format/CMakeLists.txt 
b/clang/tools/clang-ssaf-format/CMakeLists.txt
index 864fe5bc27aaa..c76756ad4770c 100644
--- a/clang/tools/clang-ssaf-format/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-format/CMakeLists.txt
@@ -10,6 +10,7 @@ add_clang_tool(clang-ssaf-format
 clang_target_link_libraries(clang-ssaf-format
   PRIVATE
   clangBasic
+  clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkTool
   )
diff --git a/clang/tools/clang-ssaf-linker/CMakeLists.txt 
b/clang/tools/clang-ssaf-linker/CMakeLists.txt
index 95db5ad2d3d08..af65aaa3b1aeb 100644
--- a/clang/tools/clang-ssaf-linker/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-linker/CMakeLists.txt
@@ -10,6 +10,7 @@ add_clang_tool(clang-ssaf-linker
 clang_target_link_libraries(clang-ssaf-linker
   PRIVATE
   clangBasic
+  clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkTool
   )
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
new file mode 100644
index 0000000000000..e108542a70b45
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
@@ -0,0 +1,375 @@
+//===- CallGraphExtractorTest.cpp 
-----------------------------------------===//
+//
+// 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 "TestFixture.h"
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/ASTEntityMapping.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/TUSummary/ExtractorRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummary.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummaryBuilder.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+#include <cassert>
+
+using namespace clang;
+using namespace ssaf;
+
+using llvm::Succeeded;
+
+namespace {
+AST_MATCHER(FunctionDecl, isPrimaryTemplate) {
+  return Node.getDescribedFunctionTemplate() != nullptr;
+}
+} // namespace
+
+static llvm::Expected<const FunctionDecl *> findFn(ASTContext &Ctx,
+                                                   StringRef FnName) {
+  using namespace ast_matchers;
+  auto Matcher =
+      functionDecl(hasName(FnName), unless(isPrimaryTemplate())).bind("decl");
+  auto Matches = match(Matcher, Ctx);
+  if (Matches.empty())
+    return llvm::createStringError(
+        "No FunctionDecl definition was found with name '" + FnName + "'");
+  auto *FD = Matches[0].getNodeAs<FunctionDecl>("decl");
+  assert(FD);
+  return FD->getCanonicalDecl();
+}
+
+namespace {
+
+struct CallGraphExtractorTest : ssaf::TestFixture {
+  TUSummary Summary =
+      BuildNamespace(BuildNamespaceKind::CompilationUnit, "Mock.cpp");
+  TUSummaryBuilder Builder = TUSummaryBuilder(Summary);
+
+  /// Creates the AST and extractor, then extracts the summaries from the AST.
+  /// This will update the \c AST \c Builder and \c Summary data members.
+  void runExtractor(StringRef Code) {
+    AST = tooling::buildASTFromCode(Code);
+    auto Consumer = makeTUSummaryExtractor("CallGraph", Builder);
+    Consumer->HandleTranslationUnit(AST->getASTContext());
+  }
+
+  /// Tries to find the \c CallGraphSummary for the \p FnName function.
+  llvm::Expected<const CallGraphSummary *>
+  findSummary(llvm::StringRef FnName) const;
+
+  /// Collects the USRs of all direct callees in CallGraphSummary \p S.
+  std::set<std::string> getDirectCalleeUSRs(const CallGraphSummary *S) const;
+
+  /// Looks up the Decls for \p FnNames, and then transforms those into USRs.
+  llvm::Expected<std::set<std::string>>
+  asUSRs(llvm::ArrayRef<StringRef> FnNames);
+
+  /// Creates a GTest matcher selecting the direct callees of summary \p S.
+  auto matchCalleeUSRs(const CallGraphSummary *S) const {
+    return llvm::HasValue(testing::Eq(getDirectCalleeUSRs(S)));
+  }
+
+private:
+  std::unique_ptr<ASTUnit> AST;
+};
+
+llvm::Expected<const CallGraphSummary *>
+CallGraphExtractorTest::findSummary(llvm::StringRef FnName) const {
+  auto MaybeFD = findFn(AST->getASTContext(), FnName);
+  if (!MaybeFD)
+    return MaybeFD.takeError();
+
+  std::optional<EntityName> EntName = getEntityName(*MaybeFD);
+  if (!EntName.has_value()) {
+    return llvm::createStringError("Failed to create an entity name for '" +
+                                   FnName + "'");
+  }
+
+  const auto &EntitiesTable = getEntities(getIdTable(Summary));
+  auto It = EntitiesTable.find(EntName.value());
+  if (It == EntitiesTable.end()) {
+    return llvm::createStringError(
+        "No entity ID was present in the entity table for '" + FnName + "'");
+  }
+  EntityId ID = It->second;
+  auto &Data = getData(Summary);
+  auto SummaryIt = Data.find(SummaryName("CallGraph"));
+  if (SummaryIt == Data.end())
+    return llvm::createStringError("There is no 'CallGraph' summary");
+  auto EntityIt = SummaryIt->second.find(ID);
+  if (EntityIt == SummaryIt->second.end()) {
+    return llvm::createStringError(
+        "There is no 'CallGraph' summary for entity ID " +
+        std::to_string(getIndex(ID)) + " aka. '" + FnName + "'");
+  }
+  return static_cast<const CallGraphSummary *>(EntityIt->second.get());
+}
+
+std::set<std::string>
+CallGraphExtractorTest::getDirectCalleeUSRs(const CallGraphSummary *S) const {
+  const std::set<EntityId> &DirectCallees = S->DirectCallees;
+  std::set<std::string> USRs;
+
+  auto GatherCalleeUSRs = [&](const EntityName &Name, EntityId Id) {
+    if (llvm::is_contained(DirectCallees, Id))
+      USRs.insert(TestFixture::getUSR(Name));
+  };
+  TestFixture::getIdTable(Summary).forEach(GatherCalleeUSRs);
+  assert(DirectCallees.size() == USRs.size());
+  return USRs;
+}
+
+llvm::Expected<std::set<std::string>>
+CallGraphExtractorTest::asUSRs(llvm::ArrayRef<StringRef> FnNames) {
+  std::set<std::string> USRs;
+  ASTContext &Ctx = AST->getASTContext();
+  for (StringRef FnName : FnNames) {
+    auto MaybeFD = findFn(Ctx, FnName);
+    if (!MaybeFD)
+      return MaybeFD.takeError();
+    std::optional<EntityName> Name = getEntityName(MaybeFD.get());
+    if (!Name.has_value()) {
+      return llvm::createStringError("Failed to get the USR of '" + FnName +
+                                     "'");
+    }
+    USRs.insert(getUSR(Name.value()));
+  }
+  assert(USRs.size() == FnNames.size());
+  return USRs;
+}
+
+TEST_F(CallGraphExtractorTest, SimpleFunctionCalls) {
+  runExtractor(R"cpp(
+    void a();
+    void b();
+    void calls_a_and_b(bool coin) {
+      if (coin)
+        a();
+      else
+        b();
+    }
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("calls_a_and_b").moveInto(S), Succeeded());
+  EXPECT_FALSE(S->HasIndirectCalls);
+  EXPECT_THAT_EXPECTED(asUSRs({"a", "b"}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, NoCallees) {
+  runExtractor(R"cpp(
+    void leaf() {}
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("leaf").moveInto(S), Succeeded());
+  EXPECT_FALSE(S->HasIndirectCalls);
+  EXPECT_TRUE(S->DirectCallees.empty());
+}
+
+TEST_F(CallGraphExtractorTest, TransitiveCalls) {
+  runExtractor(R"cpp(
+    void c() { /*empty*/ }
+    void b() { c(); }
+    void a() { b(); }
+  )cpp");
+
+  { // a calls b (not c — we only record direct callees).
+    const CallGraphSummary *SA;
+    ASSERT_THAT_ERROR(findSummary("a").moveInto(SA), Succeeded());
+    EXPECT_FALSE(SA->HasIndirectCalls);
+    EXPECT_THAT_EXPECTED(asUSRs({"b"}), matchCalleeUSRs(SA));
+  }
+
+  { // b calls c.
+    const CallGraphSummary *SB;
+    ASSERT_THAT_ERROR(findSummary("b").moveInto(SB), Succeeded());
+    EXPECT_FALSE(SB->HasIndirectCalls);
+    EXPECT_THAT_EXPECTED(asUSRs({"c"}), matchCalleeUSRs(SB));
+  }
+
+  { // c calls nothing.
+    const CallGraphSummary *SC;
+    ASSERT_THAT_ERROR(findSummary("c").moveInto(SC), Succeeded());
+    EXPECT_FALSE(SC->HasIndirectCalls);
+    EXPECT_TRUE(SC->DirectCallees.empty());
+  }
+}
+
+TEST_F(CallGraphExtractorTest, VirtualCallsAreImprecise) {
+  runExtractor(R"cpp(
+    struct Base {
+      virtual void virt();
+    };
+    struct Derived : Base {
+      void virt() override;
+    };
+    void caller(Base &Obj) {
+      Obj.virt();
+    }
+  )cpp");
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+
+  // Virtual calls are treated as indirect calls.
+  EXPECT_TRUE(S->HasIndirectCalls);
+
+  // Virtual calls should not appear in DirectCallees.
+  EXPECT_THAT_EXPECTED(asUSRs({}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, MixedDirectAndVirtualCalls) {
+  runExtractor(R"cpp(
+    void direct_target();
+    struct Base {
+      virtual void virt();
+    };
+    void caller(Base &Obj) {
+      direct_target();
+      Obj.virt();
+    }
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+  EXPECT_TRUE(S->HasIndirectCalls);
+  EXPECT_THAT_EXPECTED(asUSRs({"direct_target"}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, DeclarationsOnlyNoSummary) {
+  runExtractor(R"cpp(
+    void declared_only();
+  )cpp");
+
+  // No summary for functions without definitions.
+  EXPECT_FALSE(llvm::is_contained(getData(Summary), SummaryName("CallGraph")));
+}
+
+TEST_F(CallGraphExtractorTest, DuplicateCallees) {
+  runExtractor(R"cpp(
+    void target();
+    void caller() {
+      target();
+      target();
+      target();
+    }
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+  EXPECT_FALSE(S->HasIndirectCalls);
+
+  // Despite three calls, there's only one unique callee.
+  EXPECT_THAT_EXPECTED(asUSRs({"target"}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, NonVirtualMethodCalls) {
+  runExtractor(R"cpp(
+    struct S {
+      void method();
+    };
+    void caller() {
+      S s;
+      s.method();
+    }
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+  EXPECT_FALSE(S->HasIndirectCalls);
+  EXPECT_THAT_EXPECTED(asUSRs({"method"}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, StaticMethodCalls) {
+  runExtractor(R"cpp(
+    struct S {
+      static void staticMethod();
+    };
+    void caller() {
+      S::staticMethod();
+    }
+  )cpp");
+
+  const CallGraphSummary *S;
+  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+  EXPECT_FALSE(S->HasIndirectCalls);
+  EXPECT_THAT_EXPECTED(asUSRs({"staticMethod"}), matchCalleeUSRs(S));
+}
+
+TEST_F(CallGraphExtractorTest, DefinitionLocation) {
+  runExtractor(R"cpp(
+    void callee_with_def() {}
+    void callee_without_def();
+    void caller(int n) {
+      if (n == 0) return;
+      callee_with_def();
+      callee_without_def();
+      caller(n - 1);
+    }
+  )cpp");
+
+  {
+    const CallGraphSummary *S;
+    ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+    EXPECT_FALSE(S->HasIndirectCalls);
+    EXPECT_THAT_EXPECTED(
+        asUSRs({"caller", "callee_with_def", "callee_without_def"}),
+        matchCalleeUSRs(S));
+
+    EXPECT_EQ(S->Definition.File, "input.cc");
+    EXPECT_EQ(S->Definition.Line, 4U);
+    EXPECT_EQ(S->Definition.Column, 10U);
+  }
+
+  {
+    const CallGraphSummary *S;
+    ASSERT_THAT_ERROR(findSummary("callee_with_def").moveInto(S), Succeeded());
+    EXPECT_FALSE(S->HasIndirectCalls);
+    EXPECT_TRUE(S->DirectCallees.empty());
+
+    EXPECT_EQ(S->Definition.File, "input.cc");
+    EXPECT_EQ(S->Definition.Line, 2U);
+    EXPECT_EQ(S->Definition.Column, 10U);
+  }
+}
+
+TEST_F(CallGraphExtractorTest, PrettyName) {
+  runExtractor(R"cpp(
+    template <class T, int N>
+    void templated_function(int *) {}
+    void caller(int n) {
+      templated_function<struct TypeTag, 404>(&n);
+    }
+  )cpp");
+
+  {
+    const CallGraphSummary *S;
+    ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
+    EXPECT_FALSE(S->HasIndirectCalls);
+    EXPECT_THAT_EXPECTED(asUSRs({"templated_function"}), matchCalleeUSRs(S));
+    EXPECT_EQ(S->PrettyName, "caller(int)");
+  }
+
+  {
+    const CallGraphSummary *S;
+    ASSERT_THAT_ERROR(findSummary("templated_function").moveInto(S),
+                      Succeeded());
+    EXPECT_FALSE(S->HasIndirectCalls);
+    EXPECT_TRUE(S->DirectCallees.empty());
+    // FIXME: The template arguments are not spelled here.
+    EXPECT_EQ(S->PrettyName, "templated_function(int *)");
+  }
+}
+
+} // namespace
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt 
b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 8eeaa982daaf6..5ae0a5de35e21 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_distinct_clang_unittest(ClangScalableAnalysisTests
+  Analyses/CallGraph/CallGraphExtractorTest.cpp
   Analyses/UnsafeBufferUsage/UnsafeBufferUsageTest.cpp
   ASTEntityMappingTest.cpp
   BuildNamespaceTest.cpp
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/Registries/SummaryExtractorRegistryTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/Registries/SummaryExtractorRegistryTest.cpp
index 2018beebd53da..2ffccf1cf0694 100644
--- 
a/clang/unittests/ScalableStaticAnalysisFramework/Registries/SummaryExtractorRegistryTest.cpp
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/Registries/SummaryExtractorRegistryTest.cpp
@@ -12,6 +12,7 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummary.h"
 #include "clang/Tooling/Tooling.h"
 #include "llvm/ADT/StringRef.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <memory>
 
@@ -38,11 +39,8 @@ TEST(SummaryExtractorRegistryTest, 
EnumeratingRegistryEntries) {
     EXPECT_TRUE(Inserted);
   }
 
-  EXPECT_EQ(ActualNames, (std::set<llvm::StringRef>{
-                             "MockSummaryExtractor1",
-                             "MockSummaryExtractor2",
-                             "NoOpExtractor",
-                         }));
+  EXPECT_THAT(ActualNames, testing::IsSupersetOf({"MockSummaryExtractor1",
+                                                  "MockSummaryExtractor2"}));
 }
 
 TEST(SummaryExtractorRegistryTest, InstantiatingExtractor1) {
diff --git a/llvm/utils/gn/secondary/clang/lib/Driver/BUILD.gn 
b/llvm/utils/gn/secondary/clang/lib/Driver/BUILD.gn
index f8073d9bf1ea9..8148f656fe258 100644
--- a/llvm/utils/gn/secondary/clang/lib/Driver/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/lib/Driver/BUILD.gn
@@ -17,6 +17,7 @@ static_library("Driver") {
     "//clang/lib/DependencyScanning",
     "//clang/lib/Frontend",
     "//clang/lib/Options",
+    "//clang/lib/ScalableStaticAnalysisFramework/Analyses",
     "//clang/lib/ScalableStaticAnalysisFramework/Core",
     "//clang/lib/ScalableStaticAnalysisFramework/Frontend",
     "//llvm/include/llvm/Config:llvm-config",
diff --git a/llvm/utils/gn/secondary/clang/lib/FrontendTool/BUILD.gn 
b/llvm/utils/gn/secondary/clang/lib/FrontendTool/BUILD.gn
index 60157daa66d40..4d515b7a25ce0 100644
--- a/llvm/utils/gn/secondary/clang/lib/FrontendTool/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/lib/FrontendTool/BUILD.gn
@@ -12,6 +12,7 @@ static_library("FrontendTool") {
     "//clang/lib/Frontend",
     "//clang/lib/Frontend/Rewrite",
     "//clang/lib/Options",
+    "//clang/lib/ScalableStaticAnalysisFramework/Analyses",
     "//clang/lib/ScalableStaticAnalysisFramework/Core",
     "//clang/lib/ScalableStaticAnalysisFramework/Frontend",
     "//llvm/lib/Option",
diff --git 
a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysisFramework/Analyses/BUILD.gn
 
b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysisFramework/Analyses/BUILD.gn
index e002c3256eac7..ac62574ad8534 100644
--- 
a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysisFramework/Analyses/BUILD.gn
+++ 
b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysisFramework/Analyses/BUILD.gn
@@ -8,5 +8,8 @@ static_library("Analyses") {
     "//clang/lib/ScalableStaticAnalysisFramework/Core",
     "//llvm/lib/Support",
   ]
-  sources = [ "UnsafeBufferUsage/UnsafeBufferUsageExtractor.cpp" ]
+  sources = [
+    "CallGraph/CallGraphExtractor.cpp",
+    "UnsafeBufferUsage/UnsafeBufferUsageExtractor.cpp",
+  ]
 }
diff --git 
a/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysisFramework/BUILD.gn
 
b/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysisFramework/BUILD.gn
index edc43e6b04c98..b6eebea5e843b 100644
--- 
a/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysisFramework/BUILD.gn
+++ 
b/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysisFramework/BUILD.gn
@@ -18,8 +18,9 @@ unittest("ClangScalableAnalysisTests") {
   ]
   include_dirs = [ "." ]
   sources = [
-    "ASTEntityMappingTest.cpp",
+    "Analyses/CallGraph/CallGraphExtractorTest.cpp",
     "Analyses/UnsafeBufferUsage/UnsafeBufferUsageTest.cpp",
+    "ASTEntityMappingTest.cpp",
     "BuildNamespaceTest.cpp",
     "EntityIdTableTest.cpp",
     "EntityIdTest.cpp",

From 02be6403cc44c2b3658078b4fd6b985b7fe0b07b Mon Sep 17 00:00:00 2001
From: Balazs Benics <[email protected]>
Date: Mon, 30 Mar 2026 15:43:26 +0100
Subject: [PATCH 2/2] Rework the CallGraph summary for Virtual fns, direct
 callees, indirect calls

Admit that we can't handle in a clang::CallGraph-based implementation:

- indirect callees
- obj C message passing (similar concept to calls)
- primary templates

This is because clang::CallGraph doesn't expose any of these.
Consequently, we can't even tell if they were present or not.
---
 .../Analyses/CallGraph/CallGraphSummary.h     |  12 +-
 .../Analyses/CallGraph/CallGraphExtractor.cpp |  36 +-
 .../CallGraph/CallGraphExtractorTest.cpp      | 337 +++++++++++-------
 3 files changed, 239 insertions(+), 146 deletions(-)

diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
index 03bac3c6dd8e0..ad70218d01614 100644
--- 
a/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphSummary.h
@@ -19,6 +19,10 @@ namespace clang::ssaf {
 /// Summary of direct call-graph edges for a single function entity.
 ///
 /// Represents a function definition, and information about its callees.
+///
+/// \bug Indirect calls (e.g. function pointers) are not represented.
+/// \bug ObjCMessageExprs are not represented.
+/// \bug Primary template functions are not represented.
 struct CallGraphSummary final : public EntitySummary {
   struct Location {
     std::string File;
@@ -36,14 +40,12 @@ struct CallGraphSummary final : public EntitySummary {
   /// The set of direct callees of this function.
   std::set<EntityId> DirectCallees;
 
+  /// The set of virtual callees of this function.
+  std::set<EntityId> VirtualCallees;
+
   /// A human-readable name of the function.
   /// This is not guaranteed to be accurate or unique.
   std::string PrettyName;
-
-  /// Whether this function contains calls that could not be resolved to a
-  /// direct callee.
-  /// E.g. virtual method calls, or calls through function pointers.
-  bool HasIndirectCalls = false;
 };
 
 } // namespace clang::ssaf
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
index 09b237ccf8e3d..2877691d53b41 100644
--- 
a/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractor.cpp
@@ -50,9 +50,8 @@ void CallGraphExtractor::handleCallGraphNode(const ASTContext 
&Ctx,
                                              const CallGraphNode *N) {
   const FunctionDecl *Definition = N->getDefinition();
 
-  // Ignore templates for now.
-  if (Definition->isTemplated())
-    return;
+  // CallGraph does not work for primary templates.
+  assert(!Definition->isTemplated());
 
   auto CallerName = getEntityName(Definition);
   if (!CallerName)
@@ -69,31 +68,28 @@ void CallGraphExtractor::handleCallGraphNode(const 
ASTContext &Ctx,
 
   for (const auto &Record : N->callees()) {
     const Decl *CalleeDecl = Record.Callee->getDecl();
-    if (!CalleeDecl) {
-      FnSummary->HasIndirectCalls = true;
-      continue;
-    }
-    assert(!CalleeDecl->isTemplated());
 
-    // Objective-C methods might be replaced at runtime, so they are 
effectively
-    // indirect calls.
-    if (isa<ObjCMethodDecl>(CalleeDecl)) {
-      FnSummary->HasIndirectCalls = true;
-      continue;
-    }
+    // FIXME: `clang::CallGraph` does not consider indirect calls, thus this is
+    // never null.
+    assert(CalleeDecl);
 
-    // Treat virtual functions as indirect calls for now.
-    if (const auto *MD = dyn_cast_or_null<CXXMethodDecl>(CalleeDecl);
-        MD && MD->isVirtual()) {
-      FnSummary->HasIndirectCalls = true;
-      continue;
-    }
+    // FIXME: `clang::CallGraph` does not consider ObjCMessageExprs as calls.
+    // Consequently, they don't appear as a Callee.
+    assert(!isa<ObjCMethodDecl>(CalleeDecl));
+
+    // FIXME: `clang::CallGraph` does not create entries for primary templates.
+    assert(!CalleeDecl->isTemplated());
 
     auto CalleeName = getEntityName(CalleeDecl);
     if (!CalleeName)
       continue;
 
     EntityId CalleeId = SummaryBuilder.addEntity(*CalleeName);
+    if (const auto *MD = dyn_cast_or_null<CXXMethodDecl>(CalleeDecl);
+        MD && MD->isVirtual()) {
+      FnSummary->VirtualCallees.insert(CalleeId);
+      continue;
+    }
     FnSummary->DirectCallees.insert(CalleeId);
   }
 
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
index e108542a70b45..9e0b9e6e256a4 100644
--- 
a/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/Analyses/CallGraph/CallGraphExtractorTest.cpp
@@ -21,36 +21,105 @@
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
 #include "gtest/gtest.h"
 #include <cassert>
 
 using namespace clang;
 using namespace ssaf;
 
-using llvm::Succeeded;
-
 namespace {
 AST_MATCHER(FunctionDecl, isPrimaryTemplate) {
   return Node.getDescribedFunctionTemplate() != nullptr;
 }
 } // namespace
 
-static llvm::Expected<const FunctionDecl *> findFn(ASTContext &Ctx,
-                                                   StringRef FnName) {
+static llvm::Expected<const NamedDecl *> findDecl(ASTContext &Ctx,
+                                                  StringRef FnName) {
   using namespace ast_matchers;
   auto Matcher =
       functionDecl(hasName(FnName), unless(isPrimaryTemplate())).bind("decl");
   auto Matches = match(Matcher, Ctx);
   if (Matches.empty())
-    return llvm::createStringError(
-        "No FunctionDecl definition was found with name '" + FnName + "'");
-  auto *FD = Matches[0].getNodeAs<FunctionDecl>("decl");
-  assert(FD);
-  return FD->getCanonicalDecl();
+    return llvm::createStringError("No definition was found with name '" +
+                                   FnName + "'");
+  auto *ND = Matches[0].template getNodeAs<NamedDecl>("decl");
+  assert(ND);
+  return cast<NamedDecl>(ND->getCanonicalDecl());
+}
+
+// ============================================================================
+// PrintTo overload for readable failure messages.
+// Must live in the same namespace as Location (clang::ssaf) for ADL.
+// ============================================================================
+
+namespace clang::ssaf {
+void PrintTo(const CallGraphSummary::Location &Loc, std::ostream *OS) {
+  *OS << Loc.File << ":" << Loc.Line << ":" << Loc.Column;
 }
+void PrintTo(const CallGraphSummary &S, std::ostream *OS) {
+  *OS << "CallGraphSummary { PrettyName: '" << S.PrettyName << "'"
+      << ", Definition: ";
+  PrintTo(S.Definition, OS);
+  *OS << ", DirectCallees: " << S.DirectCallees.size()
+      << ", VirtualCallees: " << S.VirtualCallees.size() << " }";
+}
+} // namespace clang::ssaf
 
 namespace {
 
+MATCHER_P3(DefinedAt, File, Line, Column,
+           std::string(negation ? "is not" : "is") + " defined at " +
+               std::string(File) + ":" + testing::PrintToString(Line) + ":" +
+               testing::PrintToString(Column)) {
+  const auto &D = arg.Definition;
+  if (D.File != File || D.Line != Line || D.Column != Column) {
+    *result_listener << "defined at " << D.File << ":" << D.Line << ":"
+                     << D.Column;
+    return false;
+  }
+  return true;
+}
+
+MATCHER_P(HasPrettyName, Name,
+          std::string(negation ? "doesn't have" : "has") + " pretty name '" +
+              testing::PrintToString(Name) + "'") {
+  if (arg.PrettyName != std::string(Name)) {
+    *result_listener << "has pretty name '" << arg.PrettyName << "'";
+    return false;
+  }
+  return true;
+}
+
+MATCHER(HasNoDirectCallees,
+        std::string(negation ? "has" : "has no") + " direct callees") {
+  if (!arg.DirectCallees.empty()) {
+    *result_listener << "has " << arg.DirectCallees.size()
+                     << " direct callee(s)";
+    return false;
+  }
+  return true;
+}
+
+MATCHER(HasNoVirtualCallees,
+        std::string(negation ? "has" : "has no") + " virtual callees") {
+  if (!arg.VirtualCallees.empty()) {
+    *result_listener << "has " << arg.VirtualCallees.size()
+                     << " virtual callee(s)";
+    return false;
+  }
+  return true;
+}
+
+template <typename... Matchers> auto hasSummaryThat(const Matchers &...Ms) {
+  using namespace testing;
+  return llvm::HasValue(Pointee(AllOf(std::move(Ms)...)));
+}
+
+// ============================================================================
+// Test fixture
+// ============================================================================
+
 struct CallGraphExtractorTest : ssaf::TestFixture {
   TUSummary Summary =
       BuildNamespace(BuildNamespaceKind::CompilationUnit, "Mock.cpp");
@@ -58,8 +127,8 @@ struct CallGraphExtractorTest : ssaf::TestFixture {
 
   /// Creates the AST and extractor, then extracts the summaries from the AST.
   /// This will update the \c AST \c Builder and \c Summary data members.
-  void runExtractor(StringRef Code) {
-    AST = tooling::buildASTFromCode(Code);
+  void runExtractor(StringRef Code, ArrayRef<std::string> Args = {}) {
+    AST = tooling::buildASTFromCodeWithArgs(Code, Args);
     auto Consumer = makeTUSummaryExtractor("CallGraph", Builder);
     Consumer->HandleTranslationUnit(AST->getASTContext());
   }
@@ -68,29 +137,63 @@ struct CallGraphExtractorTest : ssaf::TestFixture {
   llvm::Expected<const CallGraphSummary *>
   findSummary(llvm::StringRef FnName) const;
 
-  /// Collects the USRs of all direct callees in CallGraphSummary \p S.
-  std::set<std::string> getDirectCalleeUSRs(const CallGraphSummary *S) const;
-
-  /// Looks up the Decls for \p FnNames, and then transforms those into USRs.
-  llvm::Expected<std::set<std::string>>
-  asUSRs(llvm::ArrayRef<StringRef> FnNames);
+  /// Matcher factory: matches a summary whose direct callees are exactly the
+  /// given set of function names (resolved to USRs via the entity table).
+  /// Uses \c testing::ResultOf to transform the summary's EntityId set into
+  /// USR strings before comparing with \c testing::ContainerEq.
+  auto hasDirectCallees(llvm::ArrayRef<StringRef> Names)
+      -> testing::Matcher<const CallGraphSummary &> {
+    auto MaybeUSRs = asUSRs(Names);
+    if (!MaybeUSRs) {
+      ADD_FAILURE() << "Failed to resolve callee names to USRs: "
+                    << llvm::toString(MaybeUSRs.takeError());
+      return testing::An<const CallGraphSummary &>();
+    }
+    std::set<std::string> ExpectedUSRs = std::move(*MaybeUSRs);
+    return testing::ResultOf(
+        "direct callees",
+        [this](const CallGraphSummary &S) {
+          return getUSRsForCallees(S.DirectCallees);
+        },
+        testing::ContainerEq(ExpectedUSRs));
+  }
 
-  /// Creates a GTest matcher selecting the direct callees of summary \p S.
-  auto matchCalleeUSRs(const CallGraphSummary *S) const {
-    return llvm::HasValue(testing::Eq(getDirectCalleeUSRs(S)));
+  /// Matcher factory: same as \c hasDirectCallees but for virtual callees.
+  auto hasVirtualCallees(llvm::ArrayRef<StringRef> Names)
+      -> testing::Matcher<const CallGraphSummary &> {
+    auto MaybeUSRs = asUSRs(Names);
+    if (!MaybeUSRs) {
+      ADD_FAILURE() << "Failed to resolve callee names to USRs: "
+                    << llvm::toString(MaybeUSRs.takeError());
+      return testing::A<const CallGraphSummary &>();
+    }
+    std::set<std::string> ExpectedUSRs = std::move(*MaybeUSRs);
+    return testing::ResultOf(
+        "virtual callees",
+        [this](const CallGraphSummary &S) {
+          return getUSRsForCallees(S.VirtualCallees);
+        },
+        testing::ContainerEq(ExpectedUSRs));
   }
 
 private:
   std::unique_ptr<ASTUnit> AST;
+
+  std::set<std::string>
+  getUSRsForCallees(const std::set<EntityId> &Callees) const;
+
+  /// Looks up the Decls for \p FnNames, and then transforms those into USRs.
+  llvm::Expected<std::set<std::string>>
+  asUSRs(llvm::ArrayRef<StringRef> FnNames);
 };
 
 llvm::Expected<const CallGraphSummary *>
 CallGraphExtractorTest::findSummary(llvm::StringRef FnName) const {
-  auto MaybeFD = findFn(AST->getASTContext(), FnName);
-  if (!MaybeFD)
-    return MaybeFD.takeError();
+  auto MaybeDecl = findDecl(AST->getASTContext(), FnName);
+  if (!MaybeDecl)
+    return MaybeDecl.takeError();
 
-  std::optional<EntityName> EntName = getEntityName(*MaybeFD);
+  std::optional<EntityName> EntName = getEntityName(*MaybeDecl);
   if (!EntName.has_value()) {
     return llvm::createStringError("Failed to create an entity name for '" +
                                    FnName + "'");
@@ -116,17 +219,16 @@ CallGraphExtractorTest::findSummary(llvm::StringRef 
FnName) const {
   return static_cast<const CallGraphSummary *>(EntityIt->second.get());
 }
 
-std::set<std::string>
-CallGraphExtractorTest::getDirectCalleeUSRs(const CallGraphSummary *S) const {
-  const std::set<EntityId> &DirectCallees = S->DirectCallees;
+std::set<std::string> CallGraphExtractorTest::getUSRsForCallees(
+    const std::set<EntityId> &Callees) const {
   std::set<std::string> USRs;
 
   auto GatherCalleeUSRs = [&](const EntityName &Name, EntityId Id) {
-    if (llvm::is_contained(DirectCallees, Id))
+    if (llvm::is_contained(Callees, Id))
       USRs.insert(TestFixture::getUSR(Name));
   };
   TestFixture::getIdTable(Summary).forEach(GatherCalleeUSRs);
-  assert(DirectCallees.size() == USRs.size());
+  assert(Callees.size() == USRs.size());
   return USRs;
 }
 
@@ -135,10 +237,10 @@ CallGraphExtractorTest::asUSRs(llvm::ArrayRef<StringRef> 
FnNames) {
   std::set<std::string> USRs;
   ASTContext &Ctx = AST->getASTContext();
   for (StringRef FnName : FnNames) {
-    auto MaybeFD = findFn(Ctx, FnName);
-    if (!MaybeFD)
-      return MaybeFD.takeError();
-    std::optional<EntityName> Name = getEntityName(MaybeFD.get());
+    auto MaybeDecl = findDecl(Ctx, FnName);
+    if (!MaybeDecl)
+      return MaybeDecl.takeError();
+    std::optional<EntityName> Name = getEntityName(MaybeDecl.get());
     if (!Name.has_value()) {
       return llvm::createStringError("Failed to get the USR of '" + FnName +
                                      "'");
@@ -149,6 +251,10 @@ CallGraphExtractorTest::asUSRs(llvm::ArrayRef<StringRef> 
FnNames) {
   return USRs;
 }
 
+// ============================================================================
+// Tests
+// ============================================================================
+
 TEST_F(CallGraphExtractorTest, SimpleFunctionCalls) {
   runExtractor(R"cpp(
     void a();
@@ -161,10 +267,9 @@ TEST_F(CallGraphExtractorTest, SimpleFunctionCalls) {
     }
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("calls_a_and_b").moveInto(S), Succeeded());
-  EXPECT_FALSE(S->HasIndirectCalls);
-  EXPECT_THAT_EXPECTED(asUSRs({"a", "b"}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(
+      findSummary("calls_a_and_b"),
+      hasSummaryThat(hasDirectCallees({"a", "b"}), HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, NoCallees) {
@@ -172,10 +277,9 @@ TEST_F(CallGraphExtractorTest, NoCallees) {
     void leaf() {}
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("leaf").moveInto(S), Succeeded());
-  EXPECT_FALSE(S->HasIndirectCalls);
-  EXPECT_TRUE(S->DirectCallees.empty());
+  ASSERT_THAT_EXPECTED(
+      findSummary("leaf"),
+      hasSummaryThat(HasNoDirectCallees(), HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, TransitiveCalls) {
@@ -185,26 +289,17 @@ TEST_F(CallGraphExtractorTest, TransitiveCalls) {
     void a() { b(); }
   )cpp");
 
-  { // a calls b (not c — we only record direct callees).
-    const CallGraphSummary *SA;
-    ASSERT_THAT_ERROR(findSummary("a").moveInto(SA), Succeeded());
-    EXPECT_FALSE(SA->HasIndirectCalls);
-    EXPECT_THAT_EXPECTED(asUSRs({"b"}), matchCalleeUSRs(SA));
-  }
+  // a calls b (not c — we only record direct callees).
+  ASSERT_THAT_EXPECTED(findSummary("a"), 
hasSummaryThat(hasDirectCallees({"b"}),
+                                                        
HasNoVirtualCallees()));
 
-  { // b calls c.
-    const CallGraphSummary *SB;
-    ASSERT_THAT_ERROR(findSummary("b").moveInto(SB), Succeeded());
-    EXPECT_FALSE(SB->HasIndirectCalls);
-    EXPECT_THAT_EXPECTED(asUSRs({"c"}), matchCalleeUSRs(SB));
-  }
+  // b calls c.
+  ASSERT_THAT_EXPECTED(findSummary("b"), 
hasSummaryThat(hasDirectCallees({"c"}),
+                                                        
HasNoVirtualCallees()));
 
-  { // c calls nothing.
-    const CallGraphSummary *SC;
-    ASSERT_THAT_ERROR(findSummary("c").moveInto(SC), Succeeded());
-    EXPECT_FALSE(SC->HasIndirectCalls);
-    EXPECT_TRUE(SC->DirectCallees.empty());
-  }
+  // c calls nothing.
+  ASSERT_THAT_EXPECTED(findSummary("c"), hasSummaryThat(HasNoDirectCallees(),
+                                                        
HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, VirtualCallsAreImprecise) {
@@ -219,14 +314,10 @@ TEST_F(CallGraphExtractorTest, VirtualCallsAreImprecise) {
       Obj.virt();
     }
   )cpp");
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
 
-  // Virtual calls are treated as indirect calls.
-  EXPECT_TRUE(S->HasIndirectCalls);
-
-  // Virtual calls should not appear in DirectCallees.
-  EXPECT_THAT_EXPECTED(asUSRs({}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(HasNoDirectCallees(), hasVirtualCallees({"Base::virt"})));
 }
 
 TEST_F(CallGraphExtractorTest, MixedDirectAndVirtualCalls) {
@@ -241,10 +332,9 @@ TEST_F(CallGraphExtractorTest, MixedDirectAndVirtualCalls) 
{
     }
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-  EXPECT_TRUE(S->HasIndirectCalls);
-  EXPECT_THAT_EXPECTED(asUSRs({"direct_target"}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(findSummary("caller"),
+                       hasSummaryThat(hasDirectCallees({"direct_target"}),
+                                      hasVirtualCallees({"Base::virt"})));
 }
 
 TEST_F(CallGraphExtractorTest, DeclarationsOnlyNoSummary) {
@@ -266,12 +356,10 @@ TEST_F(CallGraphExtractorTest, DuplicateCallees) {
     }
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-  EXPECT_FALSE(S->HasIndirectCalls);
-
   // Despite three calls, there's only one unique callee.
-  EXPECT_THAT_EXPECTED(asUSRs({"target"}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(hasDirectCallees({"target"}), HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, NonVirtualMethodCalls) {
@@ -285,10 +373,9 @@ TEST_F(CallGraphExtractorTest, NonVirtualMethodCalls) {
     }
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-  EXPECT_FALSE(S->HasIndirectCalls);
-  EXPECT_THAT_EXPECTED(asUSRs({"method"}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(hasDirectCallees({"method"}), HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, StaticMethodCalls) {
@@ -301,10 +388,38 @@ TEST_F(CallGraphExtractorTest, StaticMethodCalls) {
     }
   )cpp");
 
-  const CallGraphSummary *S;
-  ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-  EXPECT_FALSE(S->HasIndirectCalls);
-  EXPECT_THAT_EXPECTED(asUSRs({"staticMethod"}), matchCalleeUSRs(S));
+  ASSERT_THAT_EXPECTED(findSummary("caller"),
+                       hasSummaryThat(hasDirectCallees({"staticMethod"}),
+                                      HasNoVirtualCallees()));
+}
+
+TEST_F(CallGraphExtractorTest, FunctionPtrCall) {
+  runExtractor(R"cpp(
+    void caller(int (&fptr)()) {
+      fptr();
+    }
+  )cpp");
+
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(HasNoDirectCallees(), HasNoVirtualCallees()));
+}
+
+TEST_F(CallGraphExtractorTest, ObjCMessageExprs) {
+  runExtractor(R"cpp(
+    @interface NSString
+    - (id)stringByAppendingString:(id)str;
+    @end
+
+    void caller(void) {
+        id msg = [@"Hello" stringByAppendingString:@", World!"];
+    }
+  )cpp",
+               {"-x", "objective-c"});
+
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(HasNoDirectCallees(), HasNoVirtualCallees()));
 }
 
 TEST_F(CallGraphExtractorTest, DefinitionLocation) {
@@ -319,29 +434,16 @@ TEST_F(CallGraphExtractorTest, DefinitionLocation) {
     }
   )cpp");
 
-  {
-    const CallGraphSummary *S;
-    ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-    EXPECT_FALSE(S->HasIndirectCalls);
-    EXPECT_THAT_EXPECTED(
-        asUSRs({"caller", "callee_with_def", "callee_without_def"}),
-        matchCalleeUSRs(S));
-
-    EXPECT_EQ(S->Definition.File, "input.cc");
-    EXPECT_EQ(S->Definition.Line, 4U);
-    EXPECT_EQ(S->Definition.Column, 10U);
-  }
-
-  {
-    const CallGraphSummary *S;
-    ASSERT_THAT_ERROR(findSummary("callee_with_def").moveInto(S), Succeeded());
-    EXPECT_FALSE(S->HasIndirectCalls);
-    EXPECT_TRUE(S->DirectCallees.empty());
+  ASSERT_THAT_EXPECTED(
+      findSummary("caller"),
+      hasSummaryThat(
+          hasDirectCallees({"caller", "callee_with_def", 
"callee_without_def"}),
+          HasNoVirtualCallees(), DefinedAt("input.cc", 4U, 10U)));
 
-    EXPECT_EQ(S->Definition.File, "input.cc");
-    EXPECT_EQ(S->Definition.Line, 2U);
-    EXPECT_EQ(S->Definition.Column, 10U);
-  }
+  ASSERT_THAT_EXPECTED(findSummary("callee_with_def"),
+                       hasSummaryThat(HasNoDirectCallees(),
+                                      HasNoVirtualCallees(),
+                                      DefinedAt("input.cc", 2U, 10U)));
 }
 
 TEST_F(CallGraphExtractorTest, PrettyName) {
@@ -353,23 +455,16 @@ TEST_F(CallGraphExtractorTest, PrettyName) {
     }
   )cpp");
 
-  {
-    const CallGraphSummary *S;
-    ASSERT_THAT_ERROR(findSummary("caller").moveInto(S), Succeeded());
-    EXPECT_FALSE(S->HasIndirectCalls);
-    EXPECT_THAT_EXPECTED(asUSRs({"templated_function"}), matchCalleeUSRs(S));
-    EXPECT_EQ(S->PrettyName, "caller(int)");
-  }
+  ASSERT_THAT_EXPECTED(findSummary("caller"),
+                       hasSummaryThat(hasDirectCallees({"templated_function"}),
+                                      HasNoVirtualCallees(),
+                                      HasPrettyName("caller(int)")));
 
-  {
-    const CallGraphSummary *S;
-    ASSERT_THAT_ERROR(findSummary("templated_function").moveInto(S),
-                      Succeeded());
-    EXPECT_FALSE(S->HasIndirectCalls);
-    EXPECT_TRUE(S->DirectCallees.empty());
-    // FIXME: The template arguments are not spelled here.
-    EXPECT_EQ(S->PrettyName, "templated_function(int *)");
-  }
+  // FIXME: The template arguments are not spelled here.
+  ASSERT_THAT_EXPECTED(
+      findSummary("templated_function"),
+      hasSummaryThat(HasNoDirectCallees(), HasNoVirtualCallees(),
+                     HasPrettyName("templated_function(int *)")));
 }
 
 } // namespace

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to