[clang] [SSAF] Extract the virtual method override relation per TU (PR #213316)
Balázs Benics via cfe-commits
cfe-commits at lists.llvm.org
Fri Aug 7 04:59:10 PDT 2026
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 <benicsbalazs at gmail.com>
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 <benicsbalazs at gmail.com>
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
More information about the cfe-commits
mailing list