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
