[llvm-branch-commits] [clang] [SSAF] Group virtual method slots into override families (PR #213317)

Balázs Benics via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Fri Aug 7 04:59:09 PDT 2026


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

>From 35e38fb1ffc57cca79dc237f65691d658de99a48 Mon Sep 17 00:00:00 2001
From: Balazs Benics <benicsbalazs at gmail.com>
Date: Fri, 31 Jul 2026 16:16:55 +0100
Subject: [PATCH] [SSAF] Group virtual method slots into override families
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

A virtual call site can dispatch to any override, so the parameter and return
slots that occupy the same vtable slot across an override chain are
indistinguishable to a caller. Whole-program consumers therefore have to treat
them as one unit or they will reason about a slot that a call never actually
reaches.

Compute those units up front, keyed per slot, so consumers only need a map
lookup rather than their own traversal of the override relation. Overloads
occupy distinct vtable slots and stay in distinct families.

The family representative is the smallest EntityId in the class, which keeps
the result stable across runs.

§2 of rdar://179151603
---
 .../VirtualMethodFamily/VirtualMethodFamily.h |  46 +++
 .../BuiltinAnchorSources.def                  |   1 +
 .../Core/Model/EntityId.h                     |  11 +
 .../Analyses/CMakeLists.txt                   |   1 +
 .../VirtualMethodFamilyAnalysis.cpp           | 201 +++++++++++
 .../VirtualMethodFamilyAnalysisTest.cpp       | 336 ++++++++++++++++++
 .../ScalableStaticAnalysis/CMakeLists.txt     |   1 +
 7 files changed, 597 insertions(+)
 create mode 100644 clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysis.cpp
 create mode 100644 clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysisTest.cpp

diff --git a/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h b/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
index a39f2ffa1c9dc..b92ce61c2ea55 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h
@@ -12,7 +12,11 @@
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysis/Core/Model/SummaryName.h"
 #include "clang/ScalableStaticAnalysis/Core/TUSummary/EntitySummary.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
 #include <optional>
 #include <tuple>
 #include <vector>
@@ -46,6 +50,48 @@ struct VirtualMethodSummary final : public EntitySummary {
   }
 };
 
+struct VirtualMethodFamilyAnalysisResult final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("VirtualMethodFamilyAnalysisResult");
+  }
+
+  struct Data {
+    /// Represents the ID of the family the given parameter or return ID
+    /// corresponds to.
+    /// Right now, this ID is the "smallest" ID of the method in
+    /// the overloading set.
+    EntityId FamilyId;
+
+    /// The virtual method IDs of the param/return IDs it correspond to.
+    /// Basically, for "param" in "fun(param)" it will be "fun".
+    EntityId OwnerMethodId;
+  };
+  llvm::DenseMap<EntityId, Data> RetAndParamData;
+
+  friend bool operator==(const Data &L, const Data &R) {
+    return std::tie(L.FamilyId, L.OwnerMethodId) ==
+           std::tie(R.FamilyId, R.OwnerMethodId);
+  }
+  friend bool operator!=(const Data &L, const Data &R) { return !(L == R); }
+
+  bool operator==(const VirtualMethodFamilyAnalysisResult &Other) const {
+    return RetAndParamData == Other.RetAndParamData;
+  }
+
+  bool operator!=(const VirtualMethodFamilyAnalysisResult &Other) const {
+    return !(*this == Other);
+  }
+};
+
+/// Prints \p D as "{family=EntityId(1), owner=EntityId(2)}".
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              const VirtualMethodFamilyAnalysisResult::Data &D);
+
+/// Prints \p R as one "<param/return id> -> <data>" line per entry, ordered by
+/// the param/return id so that the output is stable across runs.
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              const VirtualMethodFamilyAnalysisResult &R);
+
 } // 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 17299a9d5e2eb..9fc9d7b3513ae 100644
--- a/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
+++ b/clang/include/clang/ScalableStaticAnalysis/BuiltinAnchorSources.def
@@ -31,5 +31,6 @@ ANCHOR(UnsafeBufferUsageAnalysisAnchorSource)
 ANCHOR(UnsafeBufferUsageExtractorAnchorSource)
 ANCHOR(UnsafeBufferUsageJSONFormatAnchorSource)
 ANCHOR(VirtualMethodEntityExtractorAnchorSource)
+ANCHOR(VirtualMethodFamilyAnalysisAnchorSource)
 
 #undef ANCHOR
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityId.h b/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityId.h
index e8e36119927c8..6acd2d8c86111 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityId.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/Model/EntityId.h
@@ -34,6 +34,7 @@ class EntityId {
   friend class TestFixture;
   friend llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
                                        const EntityId &Id);
+  friend struct llvm::DenseMapInfo<EntityId>;
 
   size_t Index;
 
@@ -51,4 +52,14 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const EntityId &Id);
 
 } // namespace clang::ssaf
 
+namespace llvm {
+template <> struct DenseMapInfo<clang::ssaf::EntityId> {
+  using EntityId = clang::ssaf::EntityId;
+  static unsigned getHashValue(EntityId Val) {
+    return densemap::detail::mix(Val.Index);
+  }
+  static bool isEqual(EntityId LHS, EntityId RHS) { return LHS == RHS; }
+};
+} // namespace llvm
+
 #endif // LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_MODEL_ENTITYID_H
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
index cc9191922309b..e83a84739d6a9 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
@@ -21,6 +21,7 @@ add_clang_library(clangScalableStaticAnalysisAnalyses
   UnsafeBufferUsage/UnsafeBufferUsageExtractor.cpp
   UnsafeBufferUsage/UnsafeBufferUsageFormat.cpp
   VirtualMethodFamily/VirtualMethodEntityExtractor.cpp
+  VirtualMethodFamily/VirtualMethodFamilyAnalysis.cpp
 
   LINK_LIBS
   clangAST
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysis.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysis.cpp
new file mode 100644
index 0000000000000..bca12773ec552
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysis.cpp
@@ -0,0 +1,201 @@
+//===- VirtualMethodFamilyAnalysis.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 "clang/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamily.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/SummaryAnalysis.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/raw_ostream.h"
+#include <cassert>
+#include <map>
+#include <optional>
+#include <utility>
+
+using namespace clang::ssaf;
+
+namespace {
+
+struct MethodFamilyUnionFind {
+  EntityId find(EntityId E);
+  void unionSets(EntityId A, EntityId B);
+
+  void seed(EntityId E, EntityId Owner) { Roots.try_emplace(E, E); }
+  void seed(EntityId Owner, const VirtualMethodSummary &S);
+
+  auto keys() const { return llvm::make_first_range(Roots); }
+
+private:
+  llvm::DenseMap<EntityId, EntityId> Roots;
+};
+
+// Keeps track of what method declared the given parameter or return value.
+struct Owners {
+  void recordOwner(EntityId Owner, const VirtualMethodSummary &S);
+  void recordOwner(EntityId E, EntityId Owner);
+
+  EntityId getOwnerOf(EntityId Id) const {
+    assert(Owners.count(Id));
+    return Owners.at(Id);
+  }
+
+private:
+  llvm::DenseMap<EntityId, EntityId> Owners;
+};
+
+class VirtualMethodFamilyAnalysis final
+    : public SummaryAnalysis<VirtualMethodFamilyAnalysisResult,
+                             VirtualMethodSummary> {
+public:
+  llvm::Error add(EntityId Id, const VirtualMethodSummary &Summary) override {
+    Data[Id] = &Summary;
+    return llvm::Error::success();
+  }
+
+  llvm::Error finalize() override;
+
+private:
+  /// Fill the \c Owners and \c Family maps.
+  void groupParamsAndReturnEntities();
+
+  /// Make the param and return IDs share a family.
+  void unionParamsAndReturnEntitiesInSummaries(const VirtualMethodSummary &LHS,
+                                               const VirtualMethodSummary &RHS);
+
+  Owners Owners;
+  MethodFamilyUnionFind Family;
+  std::map<EntityId, const VirtualMethodSummary *> Data;
+};
+} // namespace
+
+EntityId MethodFamilyUnionFind::find(EntityId E) {
+  auto It = Roots.find(E);
+  if (It == Roots.end()) {
+    Roots.try_emplace(E, E); // Self-rooted singleton.
+    return E;
+  }
+  if (It->second == E)
+    return E;
+  EntityId Root = find(It->second);
+  Roots.insert_or_assign(E, Root); // Path compression.
+  return Root;
+}
+
+void MethodFamilyUnionFind::unionSets(EntityId A, EntityId B) {
+  EntityId RootA = find(A);
+  EntityId RootB = find(B);
+  if (RootA == RootB)
+    return;
+
+  // Prefer the lexicographically-smaller rep for stable output across runs.
+  if (RootB < RootA)
+    std::swap(RootA, RootB);
+
+  Roots.insert_or_assign(RootB, RootA);
+}
+
+void MethodFamilyUnionFind::seed(EntityId Owner,
+                                 const VirtualMethodSummary &S) {
+  for (EntityId P : S.ParamEntities)
+    seed(P, Owner);
+  if (S.ReturnEntity.has_value())
+    seed(S.ReturnEntity.value(), Owner);
+}
+
+void Owners::recordOwner(EntityId Owner, const VirtualMethodSummary &S) {
+  for (EntityId P : S.ParamEntities)
+    recordOwner(P, Owner);
+  if (S.ReturnEntity.has_value())
+    recordOwner(S.ReturnEntity.value(), Owner);
+}
+
+void Owners::recordOwner(EntityId E, EntityId Owner) {
+  auto [Slot, Inserted] = Owners.try_emplace(E, Owner);
+  if (!Inserted) {
+    assert(Slot->second == Owner &&
+           "Only one Owner can be associated with an Entity");
+  }
+}
+
+void VirtualMethodFamilyAnalysis::unionParamsAndReturnEntitiesInSummaries(
+    const VirtualMethodSummary &LHS, const VirtualMethodSummary &RHS) {
+  assert(LHS.ParamEntities.size() == RHS.ParamEntities.size());
+  assert(LHS.ReturnEntity.has_value() == RHS.ReturnEntity.has_value());
+
+  using llvm::zip_equal;
+  for (auto [LParam, RParam] : zip_equal(LHS.ParamEntities, RHS.ParamEntities))
+    Family.unionSets(LParam, RParam);
+
+  if (LHS.ReturnEntity.has_value())
+    Family.unionSets(*LHS.ReturnEntity, *RHS.ReturnEntity);
+}
+
+void VirtualMethodFamilyAnalysis::groupParamsAndReturnEntities() {
+  for (const auto &[CurrId, CurrSum] : Data) {
+    Owners.recordOwner(CurrId, *CurrSum);
+
+    for (EntityId OverriddenMethodId : CurrSum->OverriddenMethods) {
+      auto BaseSumIt = Data.find(OverriddenMethodId);
+      assert(BaseSumIt != Data.end());
+      const VirtualMethodSummary &BaseSum = *BaseSumIt->second;
+      unionParamsAndReturnEntitiesInSummaries(*CurrSum, BaseSum);
+    }
+  }
+}
+
+llvm::Error VirtualMethodFamilyAnalysis::finalize() {
+  groupParamsAndReturnEntities();
+
+  auto &R = getResult();
+  for (EntityId E : Family.keys()) {
+    R.RetAndParamData.insert({E, {Family.find(E), Owners.getOwnerOf(E)}});
+  }
+  return llvm::Error::success();
+}
+
+static AnalysisRegistry::Add<VirtualMethodFamilyAnalysis>
+    RegisterAnalysis("Override-family equivalence classes for virtual methods");
+
+//===----------------------------------------------------------------------===//
+// Printing
+//===----------------------------------------------------------------------===//
+
+namespace clang::ssaf {
+
+llvm::raw_ostream &
+operator<<(llvm::raw_ostream &OS,
+           const VirtualMethodFamilyAnalysisResult::Data &D) {
+  return OS << "{family=" << D.FamilyId << ", owner=" << D.OwnerMethodId << "}";
+}
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS,
+                              const VirtualMethodFamilyAnalysisResult &R) {
+  OS << "VirtualMethodFamilyAnalysisResult with " << R.RetAndParamData.size()
+     << " entries ";
+  if (R.RetAndParamData.empty())
+    return OS << "{}";
+
+  // DenseMap iteration order depends on hashing, so sort for stable output.
+  using Entry = std::pair<EntityId, VirtualMethodFamilyAnalysisResult::Data>;
+  llvm::SmallVector<Entry> Entries(R.RetAndParamData.begin(),
+                                   R.RetAndParamData.end());
+  llvm::sort(Entries,
+             [](const Entry &L, const Entry &R) { return L.first < R.first; });
+
+  OS << "{\n";
+  for (const auto &[Id, D] : Entries)
+    OS << "  " << Id << " -> " << D << "\n";
+  return OS << "}";
+}
+
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int VirtualMethodFamilyAnalysisAnchorSource = 0;
+} // namespace clang::ssaf
diff --git a/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysisTest.cpp b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysisTest.cpp
new file mode 100644
index 0000000000000..f69784663df48
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysis/Analyses/VirtualMethodFamily/VirtualMethodFamilyAnalysisTest.cpp
@@ -0,0 +1,336 @@
+//===- VirtualMethodFamilyAnalysisTest.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/EntityLinker/LUSummary.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/WholeProgramAnalysis/AnalysisDriver.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/WPASuite.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/Support/raw_ostream.h"
+#include "llvm/TargetParser/Triple.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+#include <memory>
+#include <optional>
+#include <ostream>
+#include <string>
+#include <utility>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace clang::ssaf {
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+void PrintTo(const VirtualMethodFamilyAnalysisResult &R, std::ostream *OS) {
+  std::string Str;
+  llvm::raw_string_ostream(Str) << R;
+  *OS << Str;
+}
+} // namespace clang::ssaf
+
+namespace {
+
+class VirtualMethodFamilyAnalysisTest : public VirtualMethodFamilyTestBase {
+protected:
+  // Parses \p Code, runs the VirtualMethod extractor over it, and drives the
+  // family analysis on the extracted summaries. Must be called exactly once
+  // per test, before any of the lookup helpers below.
+  void analyze(llvm::StringRef Code) {
+    ASSERT_TRUE(runVirtualMethodExtractor(Code))
+        << "failed to build the AST or instantiate the extractor";
+
+    // Hand the extracted summaries to the analysis. The EntityIdTable is
+    // copied rather than moved: the LUSummary is consumed by the driver, but
+    // the ids in the result still have to be resolvable by name afterwards.
+    constexpr auto LinkUnitKind = BuildNamespaceKind::LinkUnit;
+    NestedBuildNamespace NS{BuildNamespace(LinkUnitKind, "TestLU")};
+    llvm::Triple Target{"arm64-apple-macosx"};
+    auto LU = std::make_unique<LUSummary>(Target, std::move(NS));
+    getIdTable(*LU) = getIdTable(tuSummary());
+    getLinkageTable(*LU) = getLinkageTable(tuSummary());
+    getData(*LU) = std::move(getData(tuSummary()));
+
+    AnalysisDriver Driver(std::move(LU));
+    auto WPAOrErr = Driver.run<VirtualMethodFamilyAnalysisResult>();
+    ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
+    WPA = std::move(*WPAOrErr);
+    auto ROrErr = WPA.get<VirtualMethodFamilyAnalysisResult>();
+    ASSERT_THAT_EXPECTED(ROrErr, llvm::Succeeded());
+    R = &*ROrErr;
+  }
+
+  EntityId method(llvm::StringRef NameOrSignature) {
+    return require(entityIdOf(AST.fn(NameOrSignature)), NameOrSignature);
+  }
+
+  EntityId param(llvm::StringRef NameOrSignature, unsigned Index = 0) {
+    return require(entityIdOf(AST.findParam(NameOrSignature, Index)),
+                   NameOrSignature);
+  }
+
+  EntityId ret(llvm::StringRef NameOrSignature) {
+    return require(returnEntityIdOf(AST.fn(NameOrSignature)), NameOrSignature);
+  }
+
+  const VirtualMethodFamilyAnalysisResult &result() const { return *R; }
+
+private:
+  VirtualMethodFamilyAnalysisResult::Data at(EntityId ParamId) const {
+    auto It = R->RetAndParamData.find(ParamId);
+    if (It != R->RetAndParamData.end())
+      return It->second;
+    ADD_FAILURE() << "no data recorded for " << ParamId;
+    // Let's just return some fallback value. The test fails anyway.
+    return {ParamId, ParamId};
+  }
+
+  // EntityId has no default constructor, so report the miss and fall back to
+  // an arbitrary id; the ADD_FAILURE() above makes the test fail regardless.
+  EntityId require(std::optional<EntityId> Id, llvm::StringRef QualifiedName) {
+    if (Id)
+      return *Id;
+    ADD_FAILURE() << "no entity extracted for '" << QualifiedName << "'";
+    const auto &Entities = getEntities(getIdTable(tuSummary()));
+    if (!Entities.empty())
+      return Entities.begin()->second;
+    return getIdTable(tuSummary()).getId(EntityName("<missing>", "", {}));
+  }
+
+  WPASuite WPA = makeWPASuite();
+  const VirtualMethodFamilyAnalysisResult *R = nullptr;
+};
+
+static VirtualMethodFamilyAnalysisResult
+createResult(llvm::ArrayRef<std::pair<EntityId, std::pair<EntityId, EntityId>>>
+                 Entries) {
+  VirtualMethodFamilyAnalysisResult Res;
+  Res.RetAndParamData.reserve(Entries.size());
+  for (const auto &[Id, Data] : Entries) {
+    auto [FamilyId, OwnerMethodId] = Data;
+    Res.RetAndParamData.insert({Id, {FamilyId, OwnerMethodId}});
+  }
+  return Res;
+}
+
+TEST_F(VirtualMethodFamilyAnalysisTest, ChainOneFamily) {
+  // Base <- Mid <- Der
+  analyze(R"cpp(
+    struct Base {
+      virtual void foo(int *p);
+    };
+    struct Mid : Base {
+      void foo(int *p) override;
+    };
+    struct Der : Mid {
+      void foo(int *p) override;
+    };
+  )cpp");
+
+  EntityId BaseFoo = method("Base::foo");
+  EntityId MidFoo = method("Mid::foo");
+  EntityId DerFoo = method("Der::foo");
+
+  EntityId BaseFooP = param("Base::foo");
+  EntityId MidFooP = param("Mid::foo");
+  EntityId DerFooP = param("Der::foo");
+
+  EntityId BaseFooR = ret("Base::foo");
+  EntityId MidFooR = ret("Mid::foo");
+  EntityId DerFooR = ret("Der::foo");
+
+  EXPECT_EQ(result(),
+            createResult({
+                // Params
+                {BaseFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/BaseFoo}},
+                {MidFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/MidFoo}},
+                {DerFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/DerFoo}},
+                // Returns
+                {BaseFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/BaseFoo}},
+                {MidFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/MidFoo}},
+                {DerFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/DerFoo}},
+            }))
+      << legend();
+}
+
+// Unrelated multiple inheritance: D::f overrides both {A::f, B::f}.
+// The joining overrider bridges the two roots into a single family.
+
+TEST_F(VirtualMethodFamilyAnalysisTest, UnrelatedMultipleInheritanceMerges) {
+  // Base1 <--
+  //          |-- Der
+  // Base2 <--
+  analyze(R"cpp(
+    struct Base1 {
+      virtual void foo(int *p);
+    };
+    struct Base2 {
+      virtual void foo(int *p);
+    };
+    struct Der : Base1, Base2 {
+      void foo(int *p) override;
+    };
+  )cpp");
+
+  EntityId Base1Foo = method("Base1::foo");
+  EntityId Base2Foo = method("Base2::foo");
+  EntityId DerFoo = method("Der::foo");
+
+  EntityId Base1FooP = param("Base1::foo");
+  EntityId Base2FooP = param("Base2::foo");
+  EntityId DerFooP = param("Der::foo");
+
+  EntityId Base1FooR = ret("Base1::foo");
+  EntityId Base2FooR = ret("Base2::foo");
+  EntityId DerFooR = ret("Der::foo");
+
+  EXPECT_EQ(
+      result(),
+      createResult({
+          // Params
+          {Base1FooP, {/*FamilyId=*/Base1FooP, /*OwnerMethodId=*/Base1Foo}},
+          {Base2FooP, {/*FamilyId=*/Base1FooP, /*OwnerMethodId=*/Base2Foo}},
+          {DerFooP, {/*FamilyId=*/Base1FooP, /*OwnerMethodId=*/DerFoo}},
+          // Returns
+          {Base1FooR, {/*FamilyId=*/Base1FooR, /*OwnerMethodId=*/Base1Foo}},
+          {Base2FooR, {/*FamilyId=*/Base1FooR, /*OwnerMethodId=*/Base2Foo}},
+          {DerFooR, {/*FamilyId=*/Base1FooR, /*OwnerMethodId=*/DerFoo}},
+      }))
+      << legend();
+}
+
+// Overloads have different vtable slots, thus they need to be treated separate.
+TEST_F(VirtualMethodFamilyAnalysisTest, OverloadsNotMerged) {
+  // Base <- Der
+  analyze(R"cpp(
+    struct Base {
+      virtual void foo(int *p);  // <-- later gets overridden
+      virtual void foo(char *p); // <-- unrelated overload
+    };
+    struct Der : Base {
+      void foo(int *p) override;
+    };
+  )cpp");
+
+  EntityId BaseFooInt = method("Base::foo(int *)");
+  EntityId DerFoo = method("Der::foo");
+
+  EntityId BaseFooIntP = param("Base::foo(int *)");
+  EntityId DerFooP = param("Der::foo");
+
+  EntityId BaseFooIntR = ret("Base::foo(int *)");
+  EntityId DerFooR = ret("Der::foo");
+
+  const auto Expected = createResult({
+      // Params
+      {BaseFooIntP, {/*FamilyId=*/BaseFooIntP, /*OwnerMethodId=*/BaseFooInt}},
+      {DerFooP, {/*FamilyId=*/BaseFooIntP, /*OwnerMethodId=*/DerFoo}},
+      // Returns
+      {BaseFooIntR, {/*FamilyId=*/BaseFooIntR, /*OwnerMethodId=*/BaseFooInt}},
+      {DerFooR, {/*FamilyId=*/BaseFooIntR, /*OwnerMethodId=*/DerFoo}},
+  });
+  // "Base::foo(char *)" is not mentioned because that is not overridden by
+  // anyone.
+  EXPECT_EQ(result(), Expected) << legend();
+}
+
+// Covariant returns have the same family.
+TEST_F(VirtualMethodFamilyAnalysisTest, CovariantReturnUnifiesReturnSlots) {
+  // Base <- Der
+  analyze(R"cpp(
+    struct Base {
+      virtual Base *clone();
+    };
+    struct Der : Base {
+      Der *clone() override; // <-- has covariant return type
+    };
+  )cpp");
+
+  EntityId BaseClone = method("Base::clone");
+  EntityId DerClone = method("Der::clone");
+
+  EntityId BaseCloneR = ret("Base::clone");
+  EntityId DerCloneR = ret("Der::clone");
+
+  EXPECT_EQ(
+      result(),
+      createResult({
+          {BaseCloneR, {/*FamilyId=*/BaseCloneR, /*OwnerMethodId=*/BaseClone}},
+          {DerCloneR, {/*FamilyId=*/BaseCloneR, /*OwnerMethodId=*/DerClone}},
+      }))
+      << legend();
+}
+
+TEST_F(VirtualMethodFamilyAnalysisTest, DiamondOneFamily) {
+  //    Base      //
+  //   /    \     //
+  // Left  Right  //
+  //   \    /     //
+  //    Dia       //
+  analyze(R"cpp(
+    struct Base {
+      virtual void foo(int *p);
+    };
+    struct Left : Base {
+      void foo(int *p) override;
+    };
+    struct Right : Base {
+      void foo(int *p) override;
+    };
+    struct Dia : Left, Right {
+      void foo(int *p) override;
+    };
+  )cpp");
+
+  EntityId BaseFoo = method("Base::foo");
+  EntityId LeftFoo = method("Left::foo");
+  EntityId RightFoo = method("Right::foo");
+  EntityId DiaFoo = method("Dia::foo");
+
+  EntityId BaseFooP = param("Base::foo");
+  EntityId LeftFooP = param("Left::foo");
+  EntityId RightFooP = param("Right::foo");
+  EntityId DiaFooP = param("Dia::foo");
+
+  EntityId BaseFooR = ret("Base::foo");
+  EntityId LeftFooR = ret("Left::foo");
+  EntityId RightFooR = ret("Right::foo");
+  EntityId DiaFooR = ret("Dia::foo");
+
+  EXPECT_EQ(
+      result(),
+      createResult({
+          // Params
+          {BaseFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/BaseFoo}},
+          {LeftFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/LeftFoo}},
+          {RightFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/RightFoo}},
+          {DiaFooP, {/*FamilyId=*/BaseFooP, /*OwnerMethodId=*/DiaFoo}},
+          // Returns
+          {BaseFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/BaseFoo}},
+          {LeftFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/LeftFoo}},
+          {RightFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/RightFoo}},
+          {DiaFooR, {/*FamilyId=*/BaseFooR, /*OwnerMethodId=*/DiaFoo}},
+      }))
+      << legend();
+}
+
+TEST_F(VirtualMethodFamilyAnalysisTest, NoFamilies) {
+  analyze(R"cpp(
+    struct A {
+      virtual void f(int *p);
+    };
+  )cpp");
+  // No methods are overridden => empty map.
+  EXPECT_EQ(result(), createResult({})) << legend();
+}
+
+} // namespace
diff --git a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
index 30909d8d24751..bdc1cf63ac025 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/VirtualMethodFamilyAnalysisTest.cpp
   Analyses/VirtualMethodFamily/VirtualMethodFamilyExtractorTest.cpp
   ASTEntityMappingTest.cpp
   BuildNamespaceTest.cpp



More information about the llvm-branch-commits mailing list