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

From be7988c43e1745db1de65c1b1d5fc58a3fe5fa77 Mon Sep 17 00:00:00 2001
From: Balazs Benics <[email protected]>
Date: Fri, 31 Jul 2026 16:14:08 +0100
Subject: [PATCH 1/2] [SSAF] Extract the virtual method override relation per
 TU
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A virtual call may dispatch to any override of its callee, so a whole-program
analysis cannot reason about a method's parameters and return value in
isolation. It needs to know which method overrides which, and which slots
that relates. Collect this per TU, so a later pass can join the related
slots into families.

JSON serialization lands separately, so the summary is not writable via
--ssaf-extract-summaries yet.

§1 of rdar://179151603
---
 .../VirtualMethodFamily/VirtualMethodFamily.h |  51 ++++
 .../BuiltinAnchorSources.def                  |   1 +
 .../Analyses/CMakeLists.txt                   |   1 +
 .../VirtualMethodEntityExtractor.cpp          |  92 +++++++
 .../VirtualMethodFamilyExtractorTest.cpp      | 228 ++++++++++++++++++
 .../VirtualMethodFamilyTestSupport.h          | 155 ++++++++++++
 .../ScalableStaticAnalysis/CMakeLists.txt     |   1 +
 .../ScalableStaticAnalysis/ParsedAST.h        | 145 +++++++++++
 8 files changed, 674 insertions(+)
 create mode 100644 
clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
 create mode 100644 
clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyTestSupport.h
 create mode 100644 clang/unittests/ScalableStaticAnalysis/ParsedAST.h

diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
 
b/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
new file mode 100644
index 0000000000000..a39f2ffa1c9dc
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
@@ -0,0 +1,51 @@
+//===- VirtualMethodFamily.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_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILY_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILY_H
+
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/EntitySummary.h"
+#include "llvm/ADT/StringRef.h"
+#include <optional>
+#include <tuple>
+#include <vector>
+
+namespace clang::ssaf {
+
+struct VirtualMethodSummary final : public EntitySummary {
+  static constexpr llvm::StringLiteral Name = "VirtualMethod";
+
+  static SummaryName summaryName() { return SummaryName(Name.str()); }
+
+  SummaryName getSummaryName() const override { return summaryName(); }
+
+  /// EntityIds of each ParmVarDecl, in source order.
+  std::vector<EntityId> ParamEntities;
+
+  /// EntityId of the synthetic return-slot entity for this method.
+  std::optional<EntityId> ReturnEntity;
+
+  /// The result of \c CXXMethodDecl::overridden_methods().
+  std::vector<EntityId> OverriddenMethods;
+
+  bool operator==(const VirtualMethodSummary &Other) const {
+    return std::tie(ParamEntities, ReturnEntity, OverriddenMethods) ==
+           std::tie(Other.ParamEntities, Other.ReturnEntity,
+                    Other.OverriddenMethods);
+  }
+
+  bool operator!=(const VirtualMethodSummary &Other) const {
+    return !(*this == Other);
+  }
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILY_H
diff --git 
a/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def 
b/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
index a128ced676ed3..17299a9d5e2eb 100644
--- a/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
+++ b/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
@@ -30,5 +30,6 @@ ANCHOR(SharedLexicalRepresentationJSONFormatAnchorSource)
 ANCHOR(UnsafeBufferUsageAnalysisAnchorSource)
 ANCHOR(UnsafeBufferUsageExtractorAnchorSource)
 ANCHOR(UnsafeBufferUsageJSONFormatAnchorSource)
+ANCHOR(VirtualMethodEntityExtractorAnchorSource)
 
 #undef ANCHOR
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
index 98ce8e799e0e0..cc9191922309b 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
@@ -20,6 +20,7 @@ add_clang_library(clangScalableStaticAnalysisAnalyses
   UnsafeBufferUsage/UnsafeBufferUsageAnalysis.cpp
   UnsafeBufferUsage/UnsafeBufferUsageExtractor.cpp
   UnsafeBufferUsage/UnsafeBufferUsageFormat.cpp
+  VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
 
   LINK_LIBS
   clangAST
diff --git 
a/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
 
b/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
new file mode 100644
index 0000000000000..e3a55487d7505
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
@@ -0,0 +1,92 @@
+//===- VirtualMethodEntityExtractor.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Extract what virtual methods override what other methods.
+// The parameters might be also important for consumers so collect those as
+// well - alongside with the ID of the return value.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DynamicRecursiveASTVisitor.h"
+#include 
"clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/ExtractorRegistry.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.h"
+#include <memory>
+#include <optional>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+class VirtualMethodEntityExtractor final : public TUSummaryExtractor,
+                                           ConstDynamicRecursiveASTVisitor {
+public:
+  explicit VirtualMethodEntityExtractor(TUSummaryBuilder &Builder)
+      : TUSummaryExtractor(Builder) {
+    ShouldVisitTemplateInstantiations = true;
+    ShouldWalkTypesOfTypeLocs = false;
+    ShouldVisitImplicitCode = false;
+    ShouldVisitLambdaBody = true;
+  }
+
+private:
+  void HandleTranslationUnit(ASTContext &Ctx) override { TraverseAST(Ctx); }
+
+  bool VisitCXXMethodDecl(const CXXMethodDecl *MD) override;
+};
+} // namespace
+
+bool VirtualMethodEntityExtractor::VisitCXXMethodDecl(const CXXMethodDecl *MD) 
{
+  if (!MD->isVirtual())
+    return true;
+
+  std::optional<EntityId> MethodId = addEntity(MD);
+  if (!MethodId)
+    return true;
+
+  auto Summary = std::make_unique<VirtualMethodSummary>();
+  Summary->ParamEntities.reserve(MD->getNumParams());
+
+  for (const ParmVarDecl *P : MD->parameters()) {
+    auto ParamId = addEntity(P);
+    if (!ParamId) {
+      // If we can't get an EntityId for a parameter, drop the entire summary
+      // rather than leaving a half-populated record.
+      return true;
+    }
+    Summary->ParamEntities.push_back(ParamId.value());
+  }
+
+  if (auto ReturnId = addEntityForReturn(MD))
+    Summary->ReturnEntity = ReturnId.value();
+
+  for (const CXXMethodDecl *Overridden : MD->overridden_methods()) {
+    // We may not be able to convert methods that are coming from system
+    // headers, so skip them gracefully.
+    if (auto OverriddenId = addEntity(Overridden))
+      Summary->OverriddenMethods.push_back(*OverriddenId);
+  }
+
+  SummaryBuilder.addSummary(MethodId.value(), std::move(Summary));
+  return true;
+}
+
+static TUSummaryExtractorRegistry::Add<VirtualMethodEntityExtractor>
+    RegisterExtractor(VirtualMethodSummary::Name,
+                      "Extract information about virtual methods");
+
+namespace clang::ssaf {
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int VirtualMethodEntityExtractorAnchorSource = 0;
+} // namespace clang::ssaf
diff --git 
a/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
new file mode 100644
index 0000000000000..1b344530b5bbc
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
@@ -0,0 +1,228 @@
+//===- VirtualMethodFamilyExtractorTest.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 "VirtualMethodFamilyTestSupport.h"
+#include 
"clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/ExtractorRegistry.h"
+#include "gtest/gtest.h"
+
+#include <set>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+using VirtualMethodFamilyExtractorTest = VirtualMethodFamilyTestBase;
+
+TEST_F(VirtualMethodFamilyExtractorTest, Registers) {
+  EXPECT_TRUE(isTUSummaryExtractorRegistered(VirtualMethodSummary::Name));
+}
+
+using VirtualMethodFamilyExtractorBasicFieldPopulationTest =
+    VirtualMethodFamilyExtractorTest;
+
+TEST_F(VirtualMethodFamilyExtractorBasicFieldPopulationTest, BaseVirtual) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    class Base {
+    public:
+      virtual void foo(int *p);
+    };
+  )cpp"));
+
+  const auto *S = getMethodSummary(AST.fn("Base::foo"));
+  ASSERT_TRUE(S);
+  EXPECT_TRUE(S->ReturnEntity.has_value());
+  EXPECT_EQ(S->ParamEntities.size(), 1u);
+  // A root virtual method overrides nothing.
+  EXPECT_TRUE(S->OverriddenMethods.empty());
+}
+
+TEST_F(VirtualMethodFamilyExtractorBasicFieldPopulationTest, PureVirtual) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    class Interface {
+    public:
+      virtual void foo(int *p) = 0;
+    };
+  )cpp"));
+
+  // A pure-virtual method is still virtual, so a summary is produced for it.
+  ASSERT_TRUE(getMethodSummary(AST.fn("Interface::foo")));
+}
+
+TEST_F(VirtualMethodFamilyExtractorBasicFieldPopulationTest,
+       NonVirtualMethodSkipped) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    class C {
+    public:
+      virtual void v();
+      void nv();
+    };
+  )cpp"));
+
+  // Only the virtual method has a summary; non-virtual is skipped.
+  EXPECT_EQ(methodSummaryCount(), 1u);
+  EXPECT_TRUE(getMethodSummary(AST.fn("C::v")));
+}
+
+TEST_F(VirtualMethodFamilyExtractorBasicFieldPopulationTest,
+       OverrideWithoutVirtualKeywordExtracted) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    class Base {
+    public:
+      virtual void foo(int *p);
+    };
+    class Derived : public Base {
+    public:
+      void foo(int *p) override;
+    };
+  )cpp"));
+
+  EXPECT_EQ(methodSummaryCount(), 2u);
+  EXPECT_TRUE(getMethodSummary(AST.fn("Base::foo")));
+  EXPECT_TRUE(getMethodSummary(AST.fn("Derived::foo")));
+}
+
+using VirtualMethodFamilyExtractorOverriddenMethodTest =
+    VirtualMethodFamilyExtractorTest;
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeBaseDerivedOverride) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct Base {
+      virtual void f(int *p);
+    };
+    struct Derived : Base {
+      void f(int *p) override;
+    };
+  )cpp"));
+  const auto *D = getMethodSummary(AST.fn("Derived::f"));
+  const auto *B = getMethodSummary(AST.fn("Base::f"));
+  ASSERT_TRUE(D);
+  ASSERT_TRUE(B);
+  auto BId = entityIdOf(AST.fn("Base::f"));
+  ASSERT_TRUE(BId.has_value());
+  ASSERT_EQ(D->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(D->OverriddenMethods[0], *BId);
+  EXPECT_TRUE(B->OverriddenMethods.empty());
+}
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgePureVirtualReabstractsOverride) {
+  // Tricky: a pure-virtual method that OVERRIDES a concrete virtual. Its edge
+  // set must be non-empty despite being pure.
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct A {
+      virtual void f(int *p);
+    };
+    struct B : A {
+      void f(int *p) = 0;
+    };
+  )cpp"));
+  const auto *Bf = getMethodSummary(AST.fn("B::f"));
+  ASSERT_TRUE(Bf);
+  auto Af = entityIdOf(AST.fn("A::f"));
+  ASSERT_TRUE(Af.has_value());
+  ASSERT_EQ(Bf->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(Bf->OverriddenMethods[0], *Af);
+}
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeMultipleInheritanceTwoEdges) {
+  // Tricky: one override occupies two independent base slots.
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct A {
+      virtual void f(int *p);
+    };
+    struct B {
+      virtual void f(int *p);
+    };
+    struct D : A, B {
+      void f(int *p) override;
+    };
+  )cpp"));
+  const auto *Df = getMethodSummary(AST.fn("D::f"));
+  ASSERT_TRUE(Df);
+  auto Af = entityIdOf(AST.fn("A::f"));
+  auto Bf = entityIdOf(AST.fn("B::f"));
+  ASSERT_TRUE(Af.has_value() && Bf.has_value());
+  ASSERT_EQ(Df->OverriddenMethods.size(), 2u);
+  std::set<EntityId> Edges(Df->OverriddenMethods.begin(),
+                           Df->OverriddenMethods.end());
+  EXPECT_EQ(Edges.count(*Af), 1u);
+  EXPECT_EQ(Edges.count(*Bf), 1u);
+}
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeOverrideLinksMatchingOverloadOnly) {
+  // Tricky: overloads must not be conflated. B::f(int*) overrides only the
+  // f(int*) base overload, never f(char*).
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct A {
+      virtual void f(int *p);
+      virtual void f(char *p);
+    };
+    struct B : A {
+      void f(int *p) override;
+    };
+  )cpp"));
+  const auto *Bf = getMethodSummary(AST.fn("B::f"));
+  ASSERT_TRUE(Bf);
+  auto AfInt = entityIdOf(AST.fn("A::f(int *)"));
+  auto AfChar = entityIdOf(AST.fn("A::f(char *)"));
+  ASSERT_TRUE(AfInt.has_value() && AfChar.has_value());
+  ASSERT_EQ(Bf->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(Bf->OverriddenMethods[0], *AfInt);
+  EXPECT_NE(Bf->OverriddenMethods[0], *AfChar);
+}
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeCovariantReturnOverride) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct Base {
+      virtual Base *clone();
+    };
+    struct Deriv : Base {
+      Deriv *clone() override;
+    };
+  )cpp"));
+  const auto *Dc = getMethodSummary(AST.fn("Deriv::clone"));
+  ASSERT_TRUE(Dc);
+  auto Bc = entityIdOf(AST.fn("Base::clone"));
+  ASSERT_TRUE(Bc.has_value());
+  ASSERT_EQ(Dc->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(Dc->OverriddenMethods[0], *Bc);
+  EXPECT_TRUE(Dc->ReturnEntity.has_value());
+}
+
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeDependentBaseTemplatePatternNoCrash) {
+  // Tricky: the primary template pattern has a dependent base; overridden_
+  // methods is unresolved there. Must not crash; the instantiation carries the
+  // edge.
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    template <class T>
+    struct Wrapper {
+      virtual void f(int *p);
+    };
+    template <class T> struct DTypeParam : T {
+      virtual void f(int *p);
+    };
+    template <class T> struct DSpec : Wrapper<T> {
+      void f(int *p) override;
+    };
+    struct Concrete {
+      virtual void f(int *p);
+    };
+    template struct DSpec<Concrete>;
+  )cpp"));
+  EXPECT_GT(methodSummaryCount(), 0u);
+}
+
+} // namespace
diff --git 
a/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyTestSupport.h
 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyTestSupport.h
new file mode 100644
index 0000000000000..122f827bdda5f
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyTestSupport.h
@@ -0,0 +1,155 @@
+//===- VirtualMethodFamilyTestSupport.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Shared fixture for the VirtualMethodFamily tests: parses a snippet, runs the
+// VirtualMethod extractor over it, and resolves declarations to the EntityIds
+// the extractor minted.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILYTESTSUPPORT_H
+#define 
LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILYTESTSUPPORT_H
+
+#include "ParsedAST.h"
+#include "TestFixture.h"
+#include "clang/Frontend/SSAFOptions.h"
+#include 
"clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h"
+#include "clang/ScalableStaticAnalysis/Core/ASTEntityMapping.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/ExtractorRegistry.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummary.h"
+#include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h"
+#include "llvm/TargetParser/Triple.h"
+
+#include <map>
+#include <optional>
+#include <string>
+
+namespace clang::ssaf {
+
+/// Base fixture for tests that need a TUSummary populated by the
+/// VirtualMethod extractor.
+class VirtualMethodFamilyTestBase : public TestFixture {
+protected:
+  ParsedAST AST;
+
+  /// Parses \p Code and runs the VirtualMethod extractor over it. Returns
+  /// false if the AST could not be built or the extractor is not registered.
+  /// Call once per test, before any of the lookups below.
+  [[nodiscard]] bool runVirtualMethodExtractor(llvm::StringRef Code) {
+    if (!AST.parse(Code))
+      return false;
+    auto Extractor =
+        makeTUSummaryExtractor(VirtualMethodSummary::Name, Builder);
+    if (!Extractor)
+      return false;
+    Extractor->HandleTranslationUnit(AST.getASTContext());
+    return true;
+  }
+
+  /// Resolves \p ND to the EntityId the extractor minted for it, or
+  /// std::nullopt if the extractor produced no entity for it.
+  std::optional<EntityId> entityIdOf(const NamedDecl *ND) const {
+    return ND ? lookup(getEntityName(ND)) : std::nullopt;
+  }
+
+  /// Resolves the return slot of \p FD to its EntityId.
+  std::optional<EntityId> returnEntityIdOf(const FunctionDecl *FD) const {
+    return FD ? lookup(getEntityNameForReturn(FD)) : std::nullopt;
+  }
+
+  /// Resolves an EntityName against the table the extractor populated.
+  std::optional<EntityId> lookup(std::optional<EntityName> Name) const {
+    if (!Name)
+      return std::nullopt;
+    const auto &Entities = getEntities(getIdTable(TUSum));
+    auto It = Entities.find(*Name);
+    if (It == Entities.end())
+      return std::nullopt;
+    return It->second;
+  }
+
+  /// Looks up the extractor's summary for \p FD, or nullptr if it produced
+  /// none for this function.
+  const VirtualMethodSummary *getMethodSummary(const FunctionDecl *FD) const {
+    auto Id = entityIdOf(FD);
+    if (!Id)
+      return nullptr;
+    const auto &Data = getData(TUSum);
+    auto SumIt = Data.find(VirtualMethodSummary::summaryName());
+    if (SumIt == Data.end())
+      return nullptr;
+    auto EIt = SumIt->second.find(*Id);
+    if (EIt == SumIt->second.end())
+      return nullptr;
+    return static_cast<const VirtualMethodSummary *>(EIt->second.get());
+  }
+
+  /// Count of method-summary entries in the TUSummary.
+  std::size_t methodSummaryCount() const {
+    const auto &Data = getData(TUSum);
+    auto It = Data.find(VirtualMethodSummary::summaryName());
+    if (It == Data.end())
+      return 0;
+    return It->second.size();
+  }
+
+  /// Maps every EntityId the extractor minted for the parsed snippet to a
+  /// readable label: "Base::foo(int *)", "Base::foo(int *)#return" or
+  /// "Base::foo(int *)#param0 'p'". Entities the extractor skipped are absent.
+  std::map<EntityId, std::string> entityLabels() const {
+    std::map<EntityId, std::string> Labels;
+    auto Add = [&](std::optional<EntityId> Id, std::string Label) {
+      if (Id)
+        Labels.insert({*Id, std::move(Label)});
+    };
+
+    for (const FunctionDecl *FD : AST.functions()) {
+      std::string Sig = ParsedAST::signatureOf(FD);
+      Add(entityIdOf(FD), Sig);
+      Add(returnEntityIdOf(FD), Sig + "#return");
+      for (unsigned I = 0, E = FD->getNumParams(); I != E; ++I) {
+        const ParmVarDecl *P = FD->getParamDecl(I);
+        std::string Label = Sig + "#param" + std::to_string(I);
+        if (!P->getName().empty())
+          Label += " '" + P->getName().str() + "'";
+        Add(entityIdOf(P), std::move(Label));
+      }
+    }
+    return Labels;
+  }
+
+  /// The entityLabels() mapping as text, to be streamed into a failing
+  /// assertion so that the EntityIds in its message can be decoded.
+  std::string legend() const {
+    std::map<EntityId, std::string> Labels = entityLabels();
+    if (Labels.empty())
+      return "\nid legend: <no entities extracted>";
+
+    std::string Result;
+    llvm::raw_string_ostream OS(Result);
+    OS << "\nid legend:";
+    for (const auto &[Id, Label] : Labels)
+      OS << "\n  " << Id << " = " << Label;
+    return Result;
+  }
+
+  TUSummary &tuSummary() { return TUSum; }
+
+private:
+  SSAFOptions Opts;
+  BuildNamespace NS{BuildNamespaceKind::CompilationUnit, "Mock.cpp"};
+  TUSummary TUSum{llvm::Triple("arm64-apple-macosx"), NS};
+  TUSummaryBuilder Builder{TUSum, Opts};
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_ANALYSES_VIRTUALMETHODFAMILY_VIRTUALMETHODFAMILYTESTSUPPORT_H
diff --git a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt 
b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
index ed3b57168b069..30909d8d24751 100644
--- a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
@@ -6,6 +6,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Analyses/SharedLexicalRepresentation/EntitySourceLocationExtractorTest.cpp
   Analyses/UnsafeBufferUsage/UnsafeBufferUsageTest.cpp
   Analyses/UnsafeBufferUsage/UnsafeBufferUsageWPATest.cpp
+  Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
   ASTEntityMappingTest.cpp
   BuildNamespaceTest.cpp
   EntityIdTableTest.cpp
diff --git a/clang/unittests/ScalableStaticAnalysis/ParsedAST.h 
b/clang/unittests/ScalableStaticAnalysis/ParsedAST.h
new file mode 100644
index 0000000000000..71f9decd6ec58
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysis/ParsedAST.h
@@ -0,0 +1,145 @@
+//===- ParsedAST.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Owns an AST parsed from a code snippet and resolves declarations in it by
+// qualified name. Held by value in a test fixture, so that fixtures which
+// already have a base class of their own can still get AST lookups.
+//
+// Unlike findDeclByName in FindDecl.h, lookups here take a *qualified* name
+// ("Base::foo") and can pick among overloads.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_PARSEDAST_H
+#define LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_PARSEDAST_H
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/ASTMatchers/ASTMatchFinder.h"
+#include "clang/ASTMatchers/ASTMatchers.h"
+#include "clang/Frontend/ASTUnit.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <string>
+
+namespace clang::ssaf {
+
+class ParsedAST {
+  std::unique_ptr<ASTUnit> AST;
+
+public:
+  /// Parses \p Code as C++17. Returns false if the AST could not be built, in
+  /// which case the lookups below all return nullptr.
+  [[nodiscard]] bool parse(llvm::StringRef Code) {
+    AST = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++17"});
+    return AST != nullptr;
+  }
+
+  explicit operator bool() const { return AST != nullptr; }
+
+  ASTContext &getASTContext() const { return AST->getASTContext(); }
+
+  /// Finds a function by qualified name, e.g. "foo" or "Base::foo". Methods
+  /// are functions too, so this finds those as well.
+  ///
+  /// To pick among overloads, append the parameter list as clang prints it:
+  /// "A::f(int *)" and "A::f(char *)" name the two overloads of A::f. A bare
+  /// name that matches more than one function is an error, not a silent pick
+  /// of the first: it reports a gtest failure and returns nullptr, as does a
+  /// name that matches nothing.
+  const FunctionDecl *fn(llvm::StringRef NameOrSignature) const {
+    using namespace ast_matchers;
+    if (!AST)
+      return nullptr;
+
+    auto [Name, Params] = NameOrSignature.split('(');
+    auto Matches =
+        match(functionDecl(hasName(Name)).bind("f"), AST->getASTContext());
+
+    llvm::SmallVector<const FunctionDecl *> Candidates;
+    for (const auto &M : Matches) {
+      const auto *FD = M.getNodeAs<FunctionDecl>("f");
+      if (Params.empty() || paramsOf(FD) == Params.rtrim(')'))
+        Candidates.push_back(FD);
+    }
+
+    if (Candidates.size() == 1)
+      return Candidates.front();
+    if (Candidates.empty()) {
+      ADD_FAILURE() << "no function named '" << NameOrSignature << "'";
+    } else {
+      ADD_FAILURE() << "'" << NameOrSignature << "' is ambiguous; it matches "
+                    << Candidates.size()
+                    << " overloads. Append the parameter list to select one, "
+                       "e.g. '"
+                    << Name << "(" << paramsOf(Candidates.front()) << ")'";
+    }
+    return nullptr;
+  }
+
+  /// Finds parameter \p Index of the function named \p NameOrSignature.
+  /// Returns nullptr if there is no such function, or if it has too few
+  /// parameters.
+  const ParmVarDecl *findParam(llvm::StringRef NameOrSignature,
+                               unsigned Index) const {
+    const FunctionDecl *FD = fn(NameOrSignature);
+    if (!FD)
+      return nullptr;
+    if (Index >= FD->getNumParams()) {
+      ADD_FAILURE() << "'" << NameOrSignature << "' has no parameter " << 
Index;
+      return nullptr;
+    }
+    return FD->getParamDecl(Index);
+  }
+
+  /// Every non-implicit function (methods included) in the parsed snippet, in
+  /// the order the matcher walks the AST. Unlike fn(), a miss is not a test
+  /// failure: this is a plain enumeration, meant for building diagnostics.
+  llvm::SmallVector<const FunctionDecl *> functions() const {
+    using namespace ast_matchers;
+    llvm::SmallVector<const FunctionDecl *> Result;
+    if (!AST)
+      return Result;
+    for (const auto &M :
+         match(functionDecl().bind("f"), AST->getASTContext())) {
+      const auto *FD = M.getNodeAs<FunctionDecl>("f");
+      if (FD && !FD->isImplicit())
+        Result.push_back(FD);
+    }
+    return Result;
+  }
+
+  /// \p FD spelled the way fn() takes it: "Base::foo(int *)".
+  static std::string signatureOf(const FunctionDecl *FD) {
+    if (!FD)
+      return "<null>";
+    return FD->getQualifiedNameAsString() + "(" + paramsOf(FD) + ")";
+  }
+
+private:
+  /// The parameter list of \p FD as clang prints it, without the parentheses:
+  /// "int *", or "unsigned long, A &".
+  static std::string paramsOf(const FunctionDecl *FD) {
+    std::string Result;
+    llvm::raw_string_ostream OS(Result);
+    llvm::interleave(
+        FD->parameters(), OS,
+        [&](const ParmVarDecl *P) { OS << P->getType().getAsString(); }, ", ");
+    return Result;
+  }
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_UNITTESTS_SCALABLESTATICANALYSIS_PARSEDAST_H

From 5e98d1a82e052ea50f1736984fdbbe161712d6d4 Mon Sep 17 00:00:00 2001
From: Balazs Benics <[email protected]>
Date: Fri, 7 Aug 2026 12:50:39 +0100
Subject: [PATCH 2/2] tmp - Test that three-level chain records only direct
 override edges

`overridden_methods()` is not transitive. This means that it describes
the directly overridden methods - which is usually a single method
(unless it overrides the same function from multiple parents).

This test case demonstrates that A::f overrides B::f which overrides C::f.
And that it's not that A::f overrides C::f without overriding B::f first.

Assisted-By: claude
---
 .../VirtualMethodFamilyExtractorTest.cpp      | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git 
a/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
index 1b344530b5bbc..8342ee3e56de7 100644
--- 
a/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
+++ 
b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
@@ -159,6 +159,42 @@ TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
   EXPECT_EQ(Edges.count(*Bf), 1u);
 }
 
+TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
+       EdgeTransitiveChainRecordsOnlyDirectOverride) {
+  ASSERT_TRUE(runVirtualMethodExtractor(R"cpp(
+    struct C {
+      virtual void f(int *p);
+    };
+    struct B : C {
+      void f(int *p) override;
+    };
+    struct A : B {
+      void f(int *p) override;
+    };
+  )cpp"));
+  const auto *Af = getMethodSummary(AST.fn("A::f"));
+  const auto *Bf = getMethodSummary(AST.fn("B::f"));
+  const auto *Cf = getMethodSummary(AST.fn("C::f"));
+  auto BfId = entityIdOf(AST.fn("B::f"));
+  auto CfId = entityIdOf(AST.fn("C::f"));
+  ASSERT_TRUE(Af);
+  ASSERT_TRUE(Bf);
+  ASSERT_TRUE(Cf);
+  ASSERT_TRUE(BfId.has_value());
+  ASSERT_TRUE(CfId.has_value());
+
+  // A::f overrides only its immediate base B::f, not the transitive C::f.
+  ASSERT_EQ(Af->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(Af->OverriddenMethods[0], *BfId);
+
+  // B::f overrides C::f.
+  ASSERT_EQ(Bf->OverriddenMethods.size(), 1u);
+  EXPECT_EQ(Bf->OverriddenMethods[0], *CfId);
+
+  // C::f is a root and overrides nothing.
+  EXPECT_TRUE(Cf->OverriddenMethods.empty());
+}
+
 TEST_F(VirtualMethodFamilyExtractorOverriddenMethodTest,
        EdgeOverrideLinksMatchingOverloadOnly) {
   // Tricky: overloads must not be conflated. B::f(int*) overrides only the

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

Reply via email to