[clang] [llvm] [SSAF][PointerFlow] Factor out and make the pointer-flow matching reusable (PR #222780)

Ziqing Luo via cfe-commits cfe-commits at lists.llvm.org
Fri Sep 11 17:04:23 PDT 2026


https://github.com/ziqingluo-90 updated https://github.com/llvm/llvm-project/pull/222780

>From 9e04b61962fa4f1c3c4cc4d6dff3a7b5e829c952 Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Thu, 10 Sep 2026 14:06:38 -0700
Subject: [PATCH 1/2] [SSAF][PointerFlow] Factor out and make the pointer-flow
 matching reusable

The PointerFlowExtractor matches AST nodes representing pointer-flows
and converts them to entity-based data structures directly. This
commit divides this procedure into two steps: 1) match and represent
AST nodes as PointerFlowPairs; 2) convert PointerFlowPairs to
entity-based edges. Therefore, other SSAF tools may use
PointerFlowPairs.

The refactoring also improves coverage: it separates pointer-type
checking from structural matching, so structural matching alone now
discovers cases that were previously missed due to overly aggressive
type checking (e.g. a record-typed call argument or return value
initialized with a braced-init-list).

Along the way, this also fixes a bug for unnamed bit-fields.

First patch for
rdar://187125348
---
 .../Analyses/PointerFlow/PointerFlowPairs.h   | 122 +++++
 .../Analyses/CMakeLists.txt                   |   1 +
 .../PointerFlow/PointerFlowExtractor.cpp      | 287 +++---------
 .../Analyses/PointerFlow/PointerFlowPairs.cpp | 300 ++++++++++++
 .../Analyses/SSAFAnalysesCommon.h             |   8 +-
 .../PointerFlow/PointerFlowPairsTest.cpp      | 433 ++++++++++++++++++
 .../ScalableStaticAnalysis/CMakeLists.txt     |   1 +
 .../ScalableStaticAnalysis/Analyses/BUILD.gn  |   1 +
 .../unittests/ScalableStaticAnalysis/BUILD.gn |   1 +
 9 files changed, 927 insertions(+), 227 deletions(-)
 create mode 100644 clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
 create mode 100644 clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.cpp
 create mode 100644 clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairsTest.cpp

diff --git a/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h b/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
new file mode 100644
index 0000000000000..06f892a7333cc
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
@@ -0,0 +1,122 @@
+//===- PointerFlowPairs.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
+//
+//===----------------------------------------------------------------------===//
+// This file provides PointerFlowPair and PointerFlowPairMatcher.
+//
+// PointerFlowPair represents an element '(l, r)' of the pointer-flow relation
+// over declarations and expressions of pointer/array type. Each pair
+// corresponds to a value-copying (or "assignment"-flavored) language construct
+// (e.g. an assignment, argument passing, a return, or an initialization).  It
+// requires that if 'l's type is refined to carry a property (e.g., buffer
+// bounds), then 'r's type must follow; otherwise the property would be
+// lost in value-copy from 'r' to 'l'.
+//
+// PointerFlowPairMatcher walks an AST node and collects the PointerFlowPairs it
+// generates. It outputs matched pairs '(l, r)' such that
+// - 'l' and 'r' have compatible types;
+// - 'l' is either a pointer or an array;
+// - 'r' may be a list-initializer, when it has an array type.
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSIS_ANALYSES_POINTERFLOW_POINTERFLOWPAIRS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSIS_ANALYSES_POINTERFLOW_POINTERFLOWPAIRS_H
+
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/TypeBase.h"
+#include "llvm/ADT/SmallVector.h"
+#include <type_traits>
+
+namespace clang::ssaf {
+
+/// Data structure representing a pointer flow.
+/// Invariant: LHS and RHS should have compatible types.
+struct PointerFlowPair {
+  /// The left-hand side of an assignment or a variable/field
+  /// definition, a formal parameter, or the function owning a return
+  /// stmt:
+  llvm::PointerUnion<const ValueDecl *, const Expr *> LHS;
+  /// The right-hand side of an assignment or a variable/field
+  /// definition, an actual argument, or the expr being returning:
+  const Expr *RHS;
+  /// True iff the left-hand side of this PointerFlowPair represents the
+  /// return entity of a callable:
+  bool IsLHSRet;
+
+  PointerFlowPair(const ValueDecl *LHS, const Expr *RHS, bool IsLHSRet = false)
+      : LHS(LHS), RHS(RHS), IsLHSRet(IsLHSRet) {
+    assert((!IsLHSRet || isa_and_nonnull<FunctionDecl>(LHS)) &&
+           "IsLHSRet -> LHS is a FunctionDecl");
+  }
+
+  PointerFlowPair(const Expr *LHS, const Expr *RHS)
+      : LHS(LHS), RHS(RHS), IsLHSRet(false) {}
+
+  /// An alternative to access the PointerUnion LHS directly---handle it using
+  /// a function object that defines:
+  /// - T operator()(const ValueDecl *, bool IsRet, Args...);
+  /// - T operator()(const Expr *, Args...);
+  template <
+      typename F, typename... Args,
+      typename T = std::invoke_result_t<F, const ValueDecl *, bool, Args...>>
+  T visitLHS(F &&Visitor, Args... ExtraArgs) const {
+    static_assert(std::is_invocable_r_v<T, F, const ValueDecl *, bool, Args...>,
+                  "Visitor(const ValueDecl *, bool, Args...) must return T");
+    static_assert(std::is_invocable_r_v<T, F, const Expr *, Args...>,
+                  "Visitor(const Expr *, Args...) must return T");
+    if (const auto *VD = LHS.dyn_cast<const ValueDecl *>())
+      return Visitor(VD, IsLHSRet, ExtraArgs...);
+    return Visitor(LHS.dyn_cast<const Expr *>(), ExtraArgs...);
+  }
+};
+
+class PointerFlowPairMatcher {
+public:
+  ASTContext &Ctx;
+  PointerFlowPairMatcher(ASTContext &Ctx) : Ctx(Ctx) {}
+
+  // FIXME: Known gaps -- the following constructs are not handled:
+  //   - Lambda captures (by-copy, by-reference, or init-capture) of a
+  //   pointer.
+  //   - Structured bindings (`auto [a, b] = pair;`) -- the per-element
+  //     `BindingDecl`s are neither `VarDecl` nor `FieldDecl`.
+
+  /// Match and collect pointer flow.
+  /// The macth function 'F' can be described by the following rules:
+  ///
+  /// F(l = r)          := (l, r), if 'l' has a pointer/array type;
+  ///                   := F(field_1, list_item_1), ..., if 'l' has a record
+  ///                                                    type and 'r' is a
+  ///                                                    list-initializer
+  /// F(foo(a, b, ...)) := F(Param_1 = a), F(Param_2 = b), ...
+  /// F(return e;)      := F(FunRet = e), where 'FunRet' is the return
+  ///                                          entity of the enclosing
+  ///                                          function
+  /// F(ctor(a, ...) : x1(y1), ... {...})
+  ///                   := F(Param_1 = a), ...,
+  ///                            F(x1 = y1), ....
+  /// F(T var = e)      := F(var = e)
+  ///
+  /// \param DynNode the node being matched.
+  /// \param Contributor the Decl that contributes \c DynNode; it is the
+  /// enclosing function decl if \c DynNode is a return stmt.
+  /// \param Result output, a set of \c PointerFlowPair matched from \c
+  /// DynNode
+  ///
+  /// Upon return, each pair '(l, r)' in \c Result is must have the following
+  /// properties:
+  /// - 'l' and 'r' have compatible types;
+  /// - 'l' is either a pointer or an array;
+  /// - 'r' may be a list-initializer when 'l' is an array
+  bool matches(const DynTypedNode &DynNode, const NamedDecl *Contributor,
+               llvm::SmallVectorImpl<PointerFlowPair> &Result) const;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSIS_ANALYSES_POINTERFLOW_POINTERFLOWPAIRS_H
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
index 98ce8e799e0e0..1e8357e843829 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/CMakeLists.txt
@@ -12,6 +12,7 @@ add_clang_library(clangScalableStaticAnalysisAnalyses
   PointerFlow/PointerFlowAnalysis.cpp
   PointerFlow/PointerFlowExtractor.cpp
   PointerFlow/PointerFlowFormat.cpp
+  PointerFlow/PointerFlowPairs.cpp
   SharedLexicalRepresentation/EntitySourceLocationExtractor.cpp
   SharedLexicalRepresentation/SharedLexicalRepresentationFormat.cpp
   SSAFAnalysesCommon.cpp
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
index 719929bd7d43a..45785a80d4603 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
@@ -17,12 +17,12 @@
 #include "clang/AST/TypeBase.h"
 #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h"
 #include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlow.h"
-#include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h"
 #include "clang/ScalableStaticAnalysis/Core/TUSummary/ExtractorRegistry.h"
-#include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryBuilder.h"
 #include "clang/ScalableStaticAnalysis/Core/TUSummary/TUSummaryExtractor.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/STLFunctionalExtras.h"
+#include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/Error.h"
 #include <memory>
 
@@ -34,62 +34,36 @@ namespace {
 using namespace clang;
 using namespace ssaf;
 
-class PointerFlowMatcher {
+class PointerFlowEdgeBuilder {
 public:
   EdgeSet Results;
-  ASTContext &Ctx;
-  TUSummaryExtractor &Extractor;
 
-  PointerFlowMatcher(ASTContext &Ctx, TUSummaryExtractor &Extractor)
+  PointerFlowEdgeBuilder(ASTContext &Ctx, TUSummaryExtractor &Extractor)
       : Ctx(Ctx), Extractor(Extractor) {}
 
-  llvm::Error matches(const DynTypedNode &DynNode, const NamedDecl *RootDecl);
-
-  llvm::Error matchesInitializerList(const ValueDecl *Base,
-                                     const Expr *InitExpr,
-                                     unsigned ArrayElementIndirectLevel = 0);
-
-  llvm::Error matchesStmt(const Stmt *S, const NamedDecl *RootDecl);
+  llvm::Error operator()(const Expr *LHS, const Expr *RHS);
 
-  llvm::Error matchesDecl(const Decl *D, const NamedDecl *RootDecl);
+  llvm::Error operator()(const ValueDecl *LHS, bool IsRet, const Expr *RHS);
 
 private:
-  llvm::Error addEdges(Expected<DeclPointerLevelVec> &&LHS,
-                       Expected<DeclPointerLevelVec> &&RHS);
-
-  Expected<DeclPointerLevelVec> toDPL(const Expr *N) const {
-    return translateDeclPointerLevel(N, Ctx, Extractor);
-  }
-
-  static DeclPointerLevel toDPL(const NamedDecl *N, bool IsRet = false) {
-    return createDeclPointerLevel(N, IsRet);
-  }
+  ASTContext &Ctx;
+  TUSummaryExtractor &Extractor;
 
-  template <typename ParmsProvider, typename ArgsProvider>
-  llvm::Error matchesArgsWithParams(unsigned ArgIdxStart, ParmsProvider *PP,
-                                    ArgsProvider *AP) {
-    unsigned ArgIdx = ArgIdxStart;
+  /// As \c RHS of PointerFlowPairs can still be list-initializers in case
+  /// (multi-d) pointer arrays, this function decomposes them recursively and
+  /// increases pointer level of \c LHS properly.
+  llvm::Error handleRHSAndAddEdges(const DeclPointerLevelVec &LHS,
+                                   const Expr *RHS,
+                                   unsigned ArrayElementIndirectLevel = 0);
 
-    for (unsigned ParmIdx = 0;
-         ParmIdx < PP->getNumParams() && ArgIdx < AP->getNumArgs();
-         ++ArgIdx, ++ParmIdx) {
-      if (const ParmVarDecl *PD = PP->getParamDecl(ParmIdx);
-          PD && hasPtrOrArrType(PD)) {
-        if (auto Err = addEdges(DeclPointerLevelVec{toDPL(PD)},
-                                toDPL(AP->getArg(ArgIdx))))
-          return Err;
-      }
-    }
-    return llvm::Error::success();
-  }
+  /// Converts DeclPointerLevelVec pairs to edges:
+  llvm::Error addEdges(const DeclPointerLevelVec &LHS,
+                       Expected<DeclPointerLevelVec> &&RHS);
 };
 
-llvm::Error PointerFlowMatcher::addEdges(Expected<DeclPointerLevelVec> &&LHS,
-                                         Expected<DeclPointerLevelVec> &&RHS) {
-  if (!LHS && !RHS)
-    return llvm::joinErrors(LHS.takeError(), RHS.takeError());
-  if (!LHS)
-    return LHS.takeError();
+llvm::Error
+PointerFlowEdgeBuilder::addEdges(const DeclPointerLevelVec &LHS,
+                                 Expected<DeclPointerLevelVec> &&RHS) {
   if (!RHS)
     return RHS.takeError();
   if (RHS->empty())
@@ -97,8 +71,8 @@ llvm::Error PointerFlowMatcher::addEdges(Expected<DeclPointerLevelVec> &&LHS,
 
   std::vector<DeclPointerLevelVec> LVecs, RVecs;
 
-  LVecs.reserve(LHS->size());
-  for (const auto &L : *LHS)
+  LVecs.reserve(LHS.size());
+  for (const auto &L : LHS)
     LVecs.push_back(elaborateHigherDeclPointerLevels(L));
   RVecs.reserve(RHS->size());
   for (const auto &R : *RHS)
@@ -132,189 +106,45 @@ llvm::Error PointerFlowMatcher::addEdges(Expected<DeclPointerLevelVec> &&LHS,
   return llvm::Error::success();
 }
 
-/// Match and extract pointer flow.
-/// The extraction function 'XF' can be described by the following rules:
-///
-/// XF(l = r)               := addEdges(toDPL(l), toDPL(r))
-/// XF(foo(a, b, ...))      := XF(Param_1 = a), XF(Param_2 = b), ...
-/// XF(return e;)           := XF(FunRet = e), where 'FunRet' is the return
-///                                            entity of the enclosing
-///                                            function
-/// XF(ctor(a, ...) : x1(y1), ... {...})
-///                         := XF(Param_1 = a), ...,
-///                            XF(x1 = y1), ...,
-///                            ctor's body will be visited separately.
-/// XF(T var = e)           := XF(var = e)
-/// XF(T var = init-list)   := see \ref
-///                            PointerFlowMatcher::matchesInitializerList
-llvm::Error PointerFlowMatcher::matches(const DynTypedNode &DynNode,
-                                        const NamedDecl *RootDecl) {
-  if (const Stmt *S = DynNode.get<Stmt>())
-    return matchesStmt(S, RootDecl);
-  if (const Decl *D = DynNode.get<Decl>())
-    return matchesDecl(D, RootDecl);
-  return llvm::Error::success();
-}
-
-llvm::Error PointerFlowMatcher::matchesStmt(const Stmt *S,
-                                            const NamedDecl *RootDecl) {
-  // Match 'p = q' whenever it has pointer or array type:
-  if (const auto *BO = dyn_cast<BinaryOperator>(S);
-      BO && BO->getOpcode() == BO_Assign && hasPtrOrArrType(BO)) {
-    return addEdges(toDPL(BO->getLHS()), toDPL(BO->getRHS()));
-  }
-
-  // Match arg-to-param passing (in CallExpr) for any pointer type argument:
-  if (const auto *CE = dyn_cast<CallExpr>(S)) {
-    const FunctionDecl *FD = CE->getDirectCallee();
-
-    if (!FD)
-      return llvm::Error::success();
-
-    unsigned ArgIdx = 0;
-
-    if (isa<CXXOperatorCallExpr>(CE))
-      if (auto *MD = dyn_cast<CXXMethodDecl>(FD);
-          MD && !MD->isExplicitObjectMemberFunction())
-        ArgIdx = 1;
-    return matchesArgsWithParams(ArgIdx, FD, CE);
-  }
-  // Match arg-to-param passing (in CXXConstructExpr) for any pointer type
-  // argument:
-  if (const auto *CCE = dyn_cast<CXXConstructExpr>(S)) {
-    return matchesArgsWithParams(/*ArgIdxStart=*/0, CCE->getConstructor(), CCE);
-  }
-  if (const auto *RS = dyn_cast<ReturnStmt>(S)) {
-    const Expr *RetExpr = RS->getRetValue();
-    if (!RetExpr || !hasPtrOrArrType(RetExpr))
+llvm::Error
+PointerFlowEdgeBuilder::handleRHSAndAddEdges(
+    const DeclPointerLevelVec &LHS, const Expr *RHS,
+    unsigned ArrayElementIndirectLevel) {
+  const auto *ILE = dyn_cast<InitListExpr>(RHS);
+  if (!ILE) {
+    if (!hasPtrOrArrType(RHS))
       return llvm::Error::success();
-    return addEdges(DeclPointerLevelVec{toDPL(RootDecl, true)}, toDPL(RetExpr));
-  }
-  return llvm::Error::success();
-}
 
-llvm::Error PointerFlowMatcher::matchesDecl(const Decl *D,
-                                            const NamedDecl *RootDecl) {
-  const Expr *InitExpr = nullptr;
+    // Leaf: raise a copy of LHS by the array depth reached, then add edges.
+    DeclPointerLevelVec Copy = LHS;
 
-  if (const auto *VD = dyn_cast<ValueDecl>(D)) {
-    if (const auto *Var = dyn_cast<VarDecl>(VD))
-      InitExpr = Var->getInit();
-    if (const auto *Fd = dyn_cast<FieldDecl>(VD))
-      InitExpr = Fd->getInClassInitializer();
-
-    // Match initializer-list:
-    if (auto *InitLst = dyn_cast_or_null<InitListExpr>(InitExpr))
-      return matchesInitializerList(VD, InitLst);
-
-    // Match initializers to variables/fields of a pointer type:
-    if (InitExpr && hasPtrOrArrType(VD))
-      return addEdges(DeclPointerLevelVec{toDPL(VD)}, toDPL(InitExpr));
+    for (DeclPointerLevel &DPL : Copy)
+      DPL.PointerLevel += ArrayElementIndirectLevel;
+    return addEdges(Copy, translateDeclPointerLevel(RHS, Ctx, Extractor));
   }
 
-  // Match C++ constructor member-initializers:
-  if (const auto *CtorD = dyn_cast<CXXConstructorDecl>(D)) {
-    for (auto *E : CtorD->inits()) {
-      if (E->isDelegatingInitializer())
-        return matches(DynTypedNode::create(*E->getInit()), RootDecl);
-      if (const FieldDecl *FD = E->getMember(); FD && hasPtrOrArrType(FD)) {
-        if (auto Err = addEdges(DeclPointerLevelVec{toDPL(E->getMember())},
-                                toDPL(E->getInit())))
-          return Err;
-      }
-    }
-  }
-  return llvm::Error::success();
-}
+  llvm::Error Err = llvm::Error::success();
 
-// Helper function for matchesInitializerList that handles record:
-llvm::Error matchInitializerListForRecordDecl(PointerFlowMatcher &Matcher,
-                                              const RecordDecl *RecordTy,
-                                              const InitListExpr *ILE) {
-  if (auto *CXXRD = dyn_cast<CXXRecordDecl>(RecordTy))
-    if (CXXRD->getNumBases() != 0) {
-      // FIXME: support this:
-      return makeErrAtNode(
-          Matcher.Ctx, ILE,
-          "attempt to create pointer assignment edges between "
-          "CXXRecordDecls with base classes and initializer-lists");
-    }
-  // Handle union:
-  if (RecordTy->isUnion()) {
-    auto *InitField = ILE->getInitializedFieldInUnion();
-
-    if (!InitField || ILE->inits().empty())
-      return llvm::Error::success();
-    return Matcher.matchesInitializerList(InitField, ILE->getInit(0));
-  }
-  // Handle struct/class:
-  ILE = ILE->isSemanticForm() ? ILE : ILE->getSemanticForm();
-
-  auto FieldIter = RecordTy->field_begin();
-
-  assert(RecordTy->getNumFields() >= ILE->getNumInits());
-  for (auto *Init : ILE->inits())
-    if (auto Err = Matcher.matchesInitializerList(*(FieldIter++), Init))
-      return Err;
-  return llvm::Error::success();
+  // Descend one array dimension.
+  for (const auto *Init : ILE->inits())
+    Err = llvm::joinErrors(
+        std::move(Err),
+        handleRHSAndAddEdges(LHS, Init, ArrayElementIndirectLevel + 1));
+  return Err;
 }
 
-// Helper function for matchesInitializerList that handles array:
-llvm::Error matchInitializerListForArray(PointerFlowMatcher &Matcher,
-                                         const ValueDecl *Array,
-                                         const InitListExpr *ILE,
-                                         unsigned ArrayIndirectLevel = 0) {
-  for (auto *E : ILE->inits())
-    if (auto Err =
-            Matcher.matchesInitializerList(Array, E, ArrayIndirectLevel + 1))
-      return Err;
-  return llvm::Error::success();
+llvm::Error PointerFlowEdgeBuilder::operator()(const Expr *LHS,
+                                               const Expr *RHS) {
+  auto LVec = translateDeclPointerLevel(LHS, Ctx, Extractor);
+  if (!LVec)
+    return LVec.takeError();
+  return handleRHSAndAddEdges(*LVec, RHS);
 }
 
-/// Match initializer lists of the form 'Var = {a, b, c, ...}':
-///
-///   If 'Var' is a struct/union:
-///     XF(Var = {a, b, c, ...})  :=   XF(Var.field_1 = a)
-///                                    XF(Var.field_2 = b)
-///                                    ...
-///   If 'Var' is an array:
-///     XF(Var = {a, b, c, ...})  :=   XF(*Var = a)
-///                                    XF(*Var = b)
-///                                    ...
-///
-/// The process is recursive: 'a', 'b', 'c', ...  may themselves be
-/// initializer lists.  We therefore use \p ArrayElementIndirectLevel to keep
-/// track of the pointer level of the left-hand side.
-llvm::Error
-PointerFlowMatcher::matchesInitializerList(const ValueDecl *Base,
-                                           const Expr *InitExpr,
-                                           unsigned ArrayElementIndirectLevel) {
-  const InitListExpr *ILE = dyn_cast<InitListExpr>(InitExpr);
-
-  if (!ILE) {
-    if (!hasPtrOrArrType(InitExpr))
-      return llvm::Error::success();
-
-    auto BaseDPL = toDPL(Base);
-    // Apply ArrayElementIndirectLevel to BaseDPL
-    BaseDPL.PointerLevel += ArrayElementIndirectLevel;
-    return addEdges(DeclPointerLevelVec{BaseDPL}, toDPL(InitExpr));
-  }
-  // Note that `Base`'s type is NOT the real LHS type when
-  // ArrayElementIndirectLevel > 0:
-  QualType Type = InitExpr->getType();
-
-  if (auto *RD = Type->getAsRecordDecl())
-    return matchInitializerListForRecordDecl(*this, RD, ILE);
-  if (Type->isArrayType())
-    return matchInitializerListForArray(*this, Base, ILE,
-                                        ArrayElementIndirectLevel);
-
-  // Must be the case of using a initializer-list for a scalar.
-  // The initializer-list can be either singleton or empty:
-  if (ILE->getNumInits() == 0)
-    return llvm::Error::success();
-  return matchesInitializerList(Base, ILE->getInit(0));
+llvm::Error PointerFlowEdgeBuilder::operator()(const ValueDecl *LHS, bool IsRet,
+                                               const Expr *RHS) {
+  DeclPointerLevelVec LVec = {createDeclPointerLevel(LHS, IsRet)};
+  return handleRHSAndAddEdges(LVec, RHS);
 }
 
 class PointerFlowTUSummaryExtractor : public TUSummaryExtractor {
@@ -325,18 +155,23 @@ class PointerFlowTUSummaryExtractor : public TUSummaryExtractor {
   std::unique_ptr<PointerFlowEntitySummary>
   extractEntitySummary(const std::vector<const NamedDecl *> &ContributorDecls,
                        ASTContext &Ctx, TUSummaryExtractor &Extractor) {
-    PointerFlowMatcher Matcher(Ctx, Extractor);
+    ssaf::PointerFlowPairMatcher Matcher(Ctx);
+    PointerFlowEdgeBuilder Builder(Ctx, Extractor);
 
     for (const auto *Contrib : ContributorDecls) {
-      auto MatchAction = [&Matcher, Contrib](const DynTypedNode &Node) {
-        if (auto Err = Matcher.matches(Node, Contrib))
-          logWarningFromError(std::move(Err));
+      auto MatchAction = [&](const DynTypedNode &Node) {
+        llvm::SmallVector<PointerFlowPair> Pairs;
+
+        Matcher.matches(Node, Contrib, Pairs);
+        for (auto &Pair : Pairs)
+          if (auto Err = Pair.visitLHS(Builder, Pair.RHS))
+            logWarningFromError(std::move(Err));
       };
 
       findMatchesIn(Contrib, MatchAction);
     }
     return std::make_unique<PointerFlowEntitySummary>(
-        buildPointerFlowEntitySummary(std::move(Matcher.Results)));
+        buildPointerFlowEntitySummary(std::move(Builder.Results)));
   }
 
   void HandleTranslationUnit(ASTContext &Ctx) override {
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.cpp
new file mode 100644
index 0000000000000..a0b4124b10943
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.cpp
@@ -0,0 +1,300 @@
+//===- PointerFlowPairs.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/PointerFlow/PointerFlowPairs.h"
+#include "SSAFAnalysesCommon.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
+#include "clang/AST/Stmt.h"
+#include "clang/AST/TypeBase.h"
+#include "llvm/ADT/SmallVector.h"
+
+namespace {
+using namespace clang;
+using namespace ssaf;
+
+//===----------------------------------------------------------------------===//
+// Helper functions for `PointerFlowPairMatcher::matches`.
+//===----------------------------------------------------------------------===//
+
+bool findUntypedPairsInStmt(const Stmt *S, const NamedDecl *RootDecl,
+                            llvm::SmallVectorImpl<PointerFlowPair> &Result);
+bool findUntypedPairsInDecl(const Decl *D,
+                            llvm::SmallVectorImpl<PointerFlowPair> &Result);
+
+/// Dispatch \p DynNode to `findUntypedPairsInStmt`/`findUntypedPairsInDecl` and
+/// collect pairs without checking types.
+bool findUntypedPairs(const DynTypedNode &DynNode, const NamedDecl *RootDecl,
+                      llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  if (const Stmt *S = DynNode.get<Stmt>())
+    return findUntypedPairsInStmt(S, RootDecl, Result);
+  if (const Decl *D = DynNode.get<Decl>())
+    return findUntypedPairsInDecl(D, Result);
+  return false;
+}
+
+template <typename ParmsProvider, typename ArgsProvider>
+bool matchesArgsWithParams(unsigned ArgIdxStart, ParmsProvider *PP,
+                           ArgsProvider *AP,
+                           llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  unsigned ArgIdx = ArgIdxStart;
+  bool Found = false;
+
+  for (unsigned ParmIdx = 0;
+       ParmIdx < PP->getNumParams() && ArgIdx < AP->getNumArgs();
+       ++ArgIdx, ++ParmIdx) {
+    if (const ParmVarDecl *PD = PP->getParamDecl(ParmIdx)) {
+      Result.emplace_back(PD, AP->getArg(ArgIdx));
+      Found = true;
+    }
+  }
+  return Found;
+}
+
+bool findUntypedPairsInStmt(const Stmt *S, const NamedDecl *RootDecl,
+                            llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  // Match 'p = q':
+  if (const auto *BO = dyn_cast<BinaryOperator>(S);
+      BO && BO->getOpcode() == BO_Assign) {
+    Result.emplace_back(BO->getLHS(), BO->getRHS());
+    return true;
+  }
+
+  // Match arg-to-param passing (in CallExpr):
+  if (const auto *CE = dyn_cast<CallExpr>(S)) {
+    const FunctionDecl *FD = CE->getDirectCallee();
+
+    if (!FD)
+      return false;
+
+    unsigned ArgIdx = 0;
+
+    if (isa<CXXOperatorCallExpr>(CE))
+      if (const auto *MD = dyn_cast<CXXMethodDecl>(FD);
+          MD && !MD->isExplicitObjectMemberFunction())
+        ArgIdx = 1;
+    return matchesArgsWithParams(ArgIdx, FD, CE, Result);
+  }
+  // Match arg-to-param passing (in CXXConstructExpr):
+  if (const auto *CCE = dyn_cast<CXXConstructExpr>(S)) {
+    return matchesArgsWithParams(/*ArgIdxStart=*/0, CCE->getConstructor(), CCE,
+                                 Result);
+  }
+  if (const auto *RS = dyn_cast<ReturnStmt>(S)) {
+    const Expr *RetExpr = RS->getRetValue();
+    if (RetExpr)
+      if (const auto *FD = dyn_cast<FunctionDecl>(RootDecl)) {
+        Result.emplace_back(FD, RetExpr, /*IsLHSRet=*/true);
+        return true;
+      }
+    return false;
+  }
+  return false;
+}
+
+bool findUntypedPairsInDecl(const Decl *D,
+                            llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  const Expr *InitExpr = nullptr;
+
+  if (const auto *VD = dyn_cast<ValueDecl>(D)) {
+    if (const auto *Var = dyn_cast<VarDecl>(VD))
+      InitExpr = Var->getInit();
+    if (const auto *Fd = dyn_cast<FieldDecl>(VD))
+      InitExpr = Fd->getInClassInitializer();
+
+    // Match initializer-list:
+    if (const auto *InitLst = dyn_cast_or_null<InitListExpr>(InitExpr)) {
+      Result.emplace_back(VD, InitLst);
+      return true;
+    }
+    if (InitExpr) {
+      // Match initializers to variables/fields of a pointer type:
+      Result.emplace_back(VD, InitExpr);
+      return true;
+    }
+  }
+
+  bool Found = false;
+  // Match C++ constructor member-initializers here. The FieldDecl a
+  // member-initializer targets is only recorded on the CXXCtorInitializer
+  // itself, which is neither a Stmt nor a Decl,
+  if (const auto *CtorD = dyn_cast<CXXConstructorDecl>(D)) {
+    for (const auto *E : CtorD->inits()) {
+      if (const FieldDecl *FD = E->getMember()) {
+        Result.emplace_back(FD, E->getInit());
+        Found = true;
+      }
+    }
+  }
+  return Found;
+}
+
+/// Pipeline the output of `findUntypedPairs`: further decompose
+/// list-initializers around record types and filter out pairs that are not
+/// pointers or arrays.
+///
+/// Upon return, each pair in \c Result has a pointer or array type.  In
+/// addition, if its \c RHS is a list-initializer, the pair has an array type.
+///
+/// For example,
+/// - suppose 'LHS' has type 'struct S {int *x; int *y;};',
+///   - it finds in '(LHS, {1 , 2})' two pairs '(x, 1), (y, 2)'.
+///
+/// - suppose 'LHS' has type 'S[2]',
+///   - it finds in '(LHS, {{1 , 2}, {3, 4}})' four pairs
+///     '(x, 1), (y, 2), (x, 3), (y, 4)'.
+///
+/// - suppose 'LHS' has type 'int *[2]',
+///   - it finds in '(LHS, {nullptr, nullptr})' one pair
+///     '(LHS, {nullptr, nullptr})'.
+///
+/// - suppose 'LHS' has type 'int *',
+///   - it finds in '(LHS, nullptr)' one pair '(LHS, nullptr)'.
+bool matchPtrOrArrPairs(const PointerFlowPairMatcher &Matcher, PointerFlowPair Pair,
+                        llvm::SmallVectorImpl<PointerFlowPair> &Result);
+} // namespace
+
+namespace clang::ssaf {
+
+bool PointerFlowPairMatcher::matches(
+    const DynTypedNode &DynNode, const NamedDecl *RootDecl,
+    llvm::SmallVectorImpl<PointerFlowPair> &Result) const {
+  llvm::SmallVector<PointerFlowPair, 8> UntypedPairs;
+  findUntypedPairs(DynNode, RootDecl, UntypedPairs);
+
+  bool Found = false;
+  for (const PointerFlowPair &P : UntypedPairs)
+    Found |= matchPtrOrArrPairs(*this, P, Result);
+  return Found;
+}
+} // namespace clang::ssaf
+
+namespace {
+
+struct GetType {
+  QualType operator()(const ValueDecl *D, bool IsRet) const {
+    return IsRet ? cast<FunctionDecl>(D)->getReturnType() : D->getType();
+  }
+
+  QualType operator()(const Expr *E) const { return E->getType(); }
+};
+
+//===----------------------------------------------------------------------===//
+// Helper functions for `PointerFlowPairMatcher::matchPtrOrArrPairs`.
+//===----------------------------------------------------------------------===//
+
+/// Helper function for matchPtrOrArrPairs that handles record
+/// types.
+bool matchInitializerListForRecordDeclRecursive(
+    const PointerFlowPairMatcher &Matcher, const RecordDecl *RecordTy,
+    const InitListExpr *ILE, llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  if (const auto *CXXRD = dyn_cast<CXXRecordDecl>(RecordTy))
+    if (CXXRD->getNumBases() != 0) {
+      // FIXME: support this:
+      logWarningFromError(makeErrAtNode(
+          Matcher.Ctx, ILE,
+          "attempt to create pointer assignment edges between "
+          "CXXRecordDecls with base classes and initializer-lists"));
+      return false;
+    }
+  // Handle union:
+  if (RecordTy->isUnion()) {
+    const auto *InitField = ILE->getInitializedFieldInUnion();
+
+    if (!InitField || ILE->inits().empty())
+      return false;
+    return matchPtrOrArrPairs(Matcher, {InitField, ILE->getInit(0)}, Result);
+  }
+  // Handle struct/class:
+  ILE = ILE->isSemanticForm() ? ILE : ILE->getSemanticForm();
+
+  auto FieldIter = RecordTy->field_begin();
+  bool Found = false;
+
+  for (const auto *Init : ILE->inits()) {
+    // Skip unnamed bit-fields:
+    while (FieldIter != RecordTy->field_end() && FieldIter->isUnnamedBitField())
+      ++FieldIter;
+    assert(FieldIter != RecordTy->field_end());
+    Found |= matchPtrOrArrPairs(Matcher, {*(FieldIter++), Init}, Result);
+  }
+  return Found;
+}
+
+/// Helper function of `matchPtrOrArrPairs` that specifically
+/// handles an list-initializer to a (multi-dimensional) array of a record
+/// type.
+bool matchInitializerListForRecordArrayRecursive(
+    const PointerFlowPairMatcher &Matcher, const ArrayType *ArrayType,
+    const InitListExpr *ILE, llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  assert(Matcher.Ctx.getBaseElementType(ArrayType)->isRecordType() &&
+         "expected a (multi-dimensional) array of a record type");
+  auto EltTy = ArrayType->getElementType();
+  bool Found = false;
+
+  if (const auto *RD = EltTy->getAsRecordDecl()) {
+    for (const auto *Init : ILE->inits()) {
+      if (const auto *SubILE = dyn_cast<InitListExpr>(Init))
+        Found |= matchInitializerListForRecordDeclRecursive(Matcher, RD, SubILE,
+                                                            Result);
+      // No need to handle non-list-initialized records:
+    }
+    return Found;
+  }
+  if (auto *SubArrayType = Matcher.Ctx.getAsArrayType(EltTy)) {
+    for (const auto *Init : ILE->inits())
+      if (const auto *SubILE = dyn_cast<InitListExpr>(Init))
+        Found |= matchInitializerListForRecordArrayRecursive(
+            Matcher, SubArrayType, SubILE, Result);
+    return Found;
+  }
+  return false;
+}
+
+bool matchPtrOrArrPairs(const PointerFlowPairMatcher &Matcher, PointerFlowPair Pair,
+                        llvm::SmallVectorImpl<PointerFlowPair> &Result) {
+  // - Base case: `RHS` is not a InitListExpr;
+  // - Call `matchInitializerListForRecordDeclRecursive` to handle
+  //   list-initializing record;
+  // - Call `matchInitializerListForRecordArrayRecursive` to handle
+  //   list-initializing (multi-d) array of records;
+  // - Recursion on list-initialization of scalar.
+  const auto *ILE = dyn_cast<InitListExpr>(Pair.RHS);
+  QualType Type = Pair.visitLHS(GetType{});
+
+  if (!ILE) {
+    // Base case:
+    if (!hasPtrOrArrType(Type))
+      return false;
+    Result.push_back(Pair);
+    return true;
+  }
+
+  if (auto *RD = Type->getAsRecordDecl())
+    return matchInitializerListForRecordDeclRecursive(Matcher, RD, ILE, Result);
+  if (auto *ArrayType = Matcher.Ctx.getAsArrayType(Type)) {
+    auto BaseTy = Matcher.Ctx.getBaseElementType(ArrayType);
+
+    if (BaseTy->isRecordType())
+      return matchInitializerListForRecordArrayRecursive(Matcher, ArrayType,
+                                                         ILE, Result);
+    Result.push_back(Pair);
+    return true;
+  }
+
+  // Must be the case of using a initializer-list for a scalar.
+  // The initializer-list can be either singleton or empty:
+  if (ILE->getNumInits() == 0)
+    return false;
+  Pair.RHS = ILE->getInit(0);
+  return matchPtrOrArrPairs(Matcher, Pair, Result);
+}
+
+} // namespace
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h b/clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h
index 95e3411cec386..fdce8d2b92c69 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h
@@ -38,7 +38,7 @@ std::string describeJSONValue(const llvm::json::Array &A);
 std::string describeJSONValue(const llvm::json::Object &O);
 
 template <typename NodeTy, typename... Ts>
-llvm::Error makeErrAtNode(clang::ASTContext &Ctx, const NodeTy *N,
+llvm::Error makeErrAtNode(const clang::ASTContext &Ctx, const NodeTy *N,
                           llvm::StringRef Fmt, const Ts &...Args) {
   std::string LocStr = N->getBeginLoc().printToString(Ctx.getSourceManager());
   return llvm::createStringError((Fmt + " at %s").str().c_str(), Args...,
@@ -66,6 +66,12 @@ inline bool hasPtrOrArrType(const ValueDecl *D) {
       D->getType().getNonReferenceType().getCanonicalType());
 }
 
+///\return true iff QualType \c T has (reference-to) pointer or array type.
+inline bool hasPtrOrArrType(QualType T) {
+  return llvm::isa<clang::PointerType, clang::ArrayType>(
+      T.getNonReferenceType().getCanonicalType());
+}
+
 llvm::Error makeEntityNameErr(clang::ASTContext &Ctx,
                               const clang::NamedDecl *D);
 
diff --git a/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairsTest.cpp b/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairsTest.cpp
new file mode 100644
index 0000000000000..a4912156ef3ea
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairsTest.cpp
@@ -0,0 +1,433 @@
+//===- PointerFlowPairsTest.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Unit tests for `PointerFlowPair` and `PointerFlowPairMatcher`.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h"
+#include "FindDecl.h"
+#include "clang/AST/ASTTypeTraits.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/DeclCXX.h"
+#include "clang/AST/DynamicRecursiveASTVisitor.h"
+#include "clang/AST/Expr.h"
+#include "clang/Frontend/ASTUnit.h"
+#include "clang/Tooling/Tooling.h"
+#include "llvm/ADT/STLFunctionalExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include <memory>
+#include <string>
+#include <vector>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace clang::ssaf {
+// Defined in the analyses library (SSAFAnalysesCommon); forward-declared here
+// so tests can drive the matcher over a contributor exactly as the extractor
+// does.
+void findMatchesIn(const NamedDecl *Contributor,
+                   llvm::function_ref<void(const DynTypedNode &)> MatchAction);
+} // namespace clang::ssaf
+
+namespace {
+
+std::string exprToString(const Expr *E, const ASTContext &Ctx) {
+  std::string S;
+  llvm::raw_string_ostream OS(S);
+  E->printPretty(OS, /*Helper=*/nullptr, Ctx.getPrintingPolicy());
+  return S;
+}
+
+// Visitor for `PointerFlowPair::visitLHS` that pretty prints LHS
+struct RenderLHS {
+  const ASTContext &Ctx;
+
+  std::string operator()(const ValueDecl *D, bool IsRet) const {
+    return D->getNameAsString() + (IsRet ? "(ret)" : "");
+  }
+  std::string operator()(const Expr *E) const { return exprToString(E, Ctx); }
+};
+
+// Render a PointerFlowPair as "(<lhs>, <rhs>)", where both sides are
+// rendered by `RenderLHS`/`exprToString`.
+std::string pairToString(const PointerFlowPair &P, const ASTContext &Ctx) {
+  return "(" + P.visitLHS(RenderLHS{Ctx}) + ", " + exprToString(P.RHS, Ctx) +
+         ")";
+}
+
+// Finds a CXXConstructorDecl by name and parameter count, to disambiguate
+// between overloaded constructors of the same class (e.g. a delegating
+// constructor vs. its delegate).
+const CXXConstructorDecl *
+findCtorByNumParams(StringRef Name, unsigned NumParams, ASTContext &Ctx) {
+  class CtorFinder : public DynamicRecursiveASTVisitor {
+  public:
+    StringRef Name;
+    unsigned NumParams;
+    const CXXConstructorDecl *Found = nullptr;
+
+    CtorFinder(StringRef Name, unsigned NumParams)
+        : Name(Name), NumParams(NumParams) {}
+
+    bool VisitCXXConstructorDecl(CXXConstructorDecl *D) override {
+      if (D->getNameAsString() == Name && D->getNumParams() == NumParams) {
+        Found = D;
+        return false;
+      }
+      return true;
+    }
+  };
+
+  CtorFinder Finder(Name, NumParams);
+  Finder.TraverseDecl(Ctx.getTranslationUnitDecl());
+  return Finder.Found;
+}
+
+class PointerFlowPairsTest : public ::testing::Test {
+protected:
+  std::unique_ptr<ASTUnit> AST;
+
+  bool buildAST(StringRef Code,
+                std::vector<std::string> ExtraArgs = {"-Wno-unused-value"}) {
+    AST = tooling::buildASTFromCodeWithArgs(Code, ExtraArgs);
+    return AST != nullptr;
+  }
+
+  ASTContext &ctx() { return AST->getASTContext(); }
+
+  // Drives `PointerFlowPairMatcher` over `Contrib` and returns every matched
+  // pair rendered as "(<lhs>, <rhs>)", in traversal order.
+  std::vector<std::string> getPairsFor(const NamedDecl *Contrib) {
+    std::vector<std::string> Out;
+    if (!Contrib) {
+      ADD_FAILURE() << "null contributor";
+      return Out;
+    }
+    PointerFlowPairMatcher Matcher(ctx());
+    ssaf::findMatchesIn(Contrib, [&](const DynTypedNode &Node) {
+      llvm::SmallVector<PointerFlowPair> Pairs;
+      Matcher.matches(Node, Contrib, Pairs);
+      for (const PointerFlowPair &P : Pairs)
+        Out.push_back(pairToString(P, ctx()));
+    });
+    return Out;
+  }
+
+  template <typename ContributorDecl = NamedDecl>
+  std::vector<std::string> getPairs(StringRef Name) {
+    const auto *Contrib = findDeclByName<ContributorDecl>(Name, ctx());
+    if (!Contrib) {
+      ADD_FAILURE() << "failed to find Decl of \"" << Name.str() << "\"";
+      return {};
+    }
+    return getPairsFor(Contrib);
+  }
+};
+
+//////////////////////////////////////////////////////////////
+//          Basic pair matching.                             //
+//////////////////////////////////////////////////////////////
+
+TEST_F(PointerFlowPairsTest, VarDeclInit) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int *p) {
+      int *q = p;
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(q, p)"));
+}
+
+TEST_F(PointerFlowPairsTest, ReturnStmt) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    int *foo(int *p) {
+      return p;
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(foo(ret), p)"));
+}
+
+//////////////////////////////////////////////////////////////
+//          No-match.                                       //
+//////////////////////////////////////////////////////////////
+
+TEST_F(PointerFlowPairsTest, NoPairForNonPointerAssign) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int a, int b) {
+      a = b;
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::IsEmpty());
+}
+
+TEST_F(PointerFlowPairsTest, NoPairForUninitializedVar) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo() {
+      int *p;
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::IsEmpty());
+}
+
+//////////////////////////////////////////////////////////////
+//          Call / Ctor argument passing.                   //
+//////////////////////////////////////////////////////////////
+
+TEST_F(PointerFlowPairsTest, CallArgMatching) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void bar(int *param1, int y, int *param2);
+    void foo(int *p, int x, int *q) {
+      bar(p, x, q);
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"),
+              testing::UnorderedElementsAre("(param1, p)", "(param2, q)"));
+}
+
+TEST_F(PointerFlowPairsTest, CXXOperatorCallSkipsImplicitObjectArgument) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int *operator()(int *a, int *b); };
+    void foo(S obj, int *p, int *q) {
+      obj(p, q);
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"),
+              testing::UnorderedElementsAre("(a, p)", "(b, q)"));
+}
+
+TEST_F(PointerFlowPairsTest, CXXConstructExprArgMatching) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { S(int *a, int *b) {} };
+    void foo(int *p, int *q) {
+      S s{p, q};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"),
+              testing::UnorderedElementsAre("(a, p)", "(b, q)"));
+}
+
+TEST_F(PointerFlowPairsTest, MemberInitializer) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S {
+      int *member;
+      S(int *q) : member(q) {}
+    };
+  )cpp"));
+
+  EXPECT_THAT(getPairs<CXXConstructorDecl>("S"),
+              testing::ElementsAre("(member, q)"));
+}
+
+// The delegate target's arg-to-param pairs are found by the generic AST
+// traversal visiting its underlying CXXConstructExpr directly (not by an
+// explicit recursive call in `findUntypedPairsInDecl` -- an earlier version
+// did that too, double-counting these pairs, since the traversal already
+// visits every written constructor-initializer's init expr on its own).
+TEST_F(PointerFlowPairsTest, DelegatingCtorMatchesDelegateInitExactlyOnce) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S {
+      S(int *a, int *b) {}
+      S(int *p) : S(p, p) {}
+    };
+  )cpp"));
+
+  const auto *Delegator = findCtorByNumParams("S", 1, ctx());
+  ASSERT_TRUE(Delegator);
+  EXPECT_THAT(getPairsFor(Delegator),
+              testing::UnorderedElementsAre("(a, p)", "(b, p)"));
+}
+
+// Same as above, for a base-initializer's underlying CXXConstructExpr.
+TEST_F(PointerFlowPairsTest, BaseCtorInitializerMatchesBaseInitExactlyOnce) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct Base { Base(int *a) {} };
+    struct Derived : Base { Derived(int *p) : Base(p) {} };
+  )cpp"));
+
+  EXPECT_THAT(getPairs<CXXConstructorDecl>("Derived"),
+              testing::ElementsAre("(a, p)"));
+}
+
+//////////////////////////////////////////////////////////////
+//          Initializer-list decomposition.                 //
+//////////////////////////////////////////////////////////////
+
+TEST_F(PointerFlowPairsTest, RecordInitListDecomposesPerField) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int *a; int *b; };
+    void foo(int *p, int *q) {
+      S s = {p, q};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"),
+              testing::UnorderedElementsAre("(a, p)", "(b, q)"));
+}
+
+// Record decomposition applies the same way when the record-typed pair comes
+// from call-argument matching rather than a VarDecl initializer.
+TEST_F(PointerFlowPairsTest, CallArgRecordInitListDecomposesPerField) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int *a; int *b; };
+    void bar(S s);
+    void foo(int *p, int *q) {
+      bar({p, q});
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"),
+              testing::UnorderedElementsAre("(a, p)", "(b, q)"));
+}
+
+TEST_F(PointerFlowPairsTest, RecordWithBaseClassInitListIsDropped) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct Base { int *x; };
+    struct Derived : Base { int *y; };
+    void foo(int *p, int *q) {
+      Derived d = {p, q};
+    }
+  )cpp",
+                       {"-std=c++17", "-Wno-unused-value"}));
+
+  EXPECT_THAT(getPairs("foo"), testing::IsEmpty());
+}
+
+TEST_F(PointerFlowPairsTest, UnionInitListPicksActiveField) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    union U { int *x; int y; };
+    void foo(int *p) {
+      U u = {p};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(x, p)"));
+}
+
+TEST_F(PointerFlowPairsTest, UnionEmptyInitListProducesNoPair) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    union U { int *x; int y; };
+    void foo() {
+      U u = {};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::IsEmpty());
+}
+
+TEST_F(PointerFlowPairsTest, ArrayOfPointersInitListIsKeptWhole) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int *p, int *q) {
+      int *arr[] = {p, q};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(arr, {p, q})"));
+}
+
+TEST_F(PointerFlowPairsTest, ArrayOfScalarsInitListIsAlsoKeptWhole) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int x, int y) {
+      int arr[] = {x, y};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(arr, {x, y})"));
+}
+
+TEST_F(PointerFlowPairsTest, CallArgArrayInitListBoundToReferenceIsKeptWhole) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void bar(int * const (&param)[2]);
+    void foo(int *p, int *q) {
+      bar({p, q});
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(param, {p, q})"));
+}
+
+TEST_F(PointerFlowPairsTest, RecordFieldOfArrayOfPointersKeepsInitListWhole) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int *arr[2]; };
+    S foo(int *p, int *q) {
+      return {p, q};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(arr, {p, q})"));
+}
+
+// Unlike an array of pointers, an array of records IS decomposed per-element,
+// since each element is itself a record init-list.
+TEST_F(PointerFlowPairsTest, ArrayOfRecordsInitListIsDecomposedPerElement) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int *a; int *b; };
+    void foo(int *p, int *q, int *r, int *s) {
+      S arr[] = {{p, q}, {r, s}};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::UnorderedElementsAre(
+                                   "(a, p)", "(b, q)", "(a, r)", "(b, s)"));
+}
+
+// An unnamed bit-field consumes no slot in the semantic InitListExpr, so the
+// field after it must still be paired with the right initializer.
+TEST_F(PointerFlowPairsTest, StructInitListWithUnnamedBitFieldSkipsBitField) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    struct S { int a; int : 4; int *p; };
+    void foo(int a, int *q) {
+      S s = {a, q};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(p, q)"));
+}
+
+TEST_F(PointerFlowPairsTest, EmptyInitListForScalarProducesNoPair) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo() {
+      int *q = {};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::IsEmpty());
+}
+
+TEST_F(PointerFlowPairsTest, SingletonInitListForScalarRecursesToElement) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int *p) {
+      int *q = {p};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(q, p)"));
+}
+
+TEST_F(PointerFlowPairsTest, AssignRHSInitListPeelsSingletonForScalarLHS) {
+  ASSERT_TRUE(buildAST(R"cpp(
+    void foo(int *p, int *q, int *r) {
+      q = {p};
+      r = {};
+    }
+  )cpp"));
+
+  EXPECT_THAT(getPairs("foo"), testing::ElementsAre("(q, p)"));
+}
+
+} // namespace
diff --git a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
index 8c339bee60f63..556c0da1de74b 100644
--- a/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysis/CMakeLists.txt
@@ -1,5 +1,6 @@
 add_distinct_clang_unittest(ClangScalableAnalysisTests
   Analyses/EntityPointerLevel/EntityPointerLevelTest.cpp
+  Analyses/PointerFlow/PointerFlowPairsTest.cpp
   Analyses/PointerFlow/PointerFlowTest.cpp
   Analyses/PointerFlow/PointerFlowWPATest.cpp
   Analyses/CallGraph/CallGraphExtractorTest.cpp
diff --git a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/Analyses/BUILD.gn b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/Analyses/BUILD.gn
index 1fcd8177c3844..984dc1d7fae30 100644
--- a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/Analyses/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/Analyses/BUILD.gn
@@ -18,6 +18,7 @@ static_library("Analyses") {
     "PointerFlow/PointerFlowAnalysis.cpp",
     "PointerFlow/PointerFlowExtractor.cpp",
     "PointerFlow/PointerFlowFormat.cpp",
+    "PointerFlow/PointerFlowPairs.cpp",
     "SSAFAnalysesCommon.cpp",
     "SharedLexicalRepresentation/EntitySourceLocationExtractor.cpp",
     "SharedLexicalRepresentation/SharedLexicalRepresentationFormat.cpp",
diff --git a/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysis/BUILD.gn b/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysis/BUILD.gn
index 0da386d1f3f58..27932a6309d30 100644
--- a/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysis/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/unittests/ScalableStaticAnalysis/BUILD.gn
@@ -23,6 +23,7 @@ unittest("ClangScalableAnalysisTests") {
     "ASTEntityMappingTest.cpp",
     "Analyses/CallGraph/CallGraphExtractorTest.cpp",
     "Analyses/EntityPointerLevel/EntityPointerLevelTest.cpp",
+    "Analyses/PointerFlow/PointerFlowPairsTest.cpp",
     "Analyses/PointerFlow/PointerFlowTest.cpp",
     "Analyses/PointerFlow/PointerFlowWPATest.cpp",
     "Analyses/SharedLexicalRepresentation/EntitySourceLocationExtractorTest.cpp",

>From d5c1b52715d24b040f3500fd6ce408473ddb3125 Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Fri, 11 Sep 2026 16:59:12 -0700
Subject: [PATCH 2/2] fix clang-format

---
 .../Analyses/PointerFlow/PointerFlowExtractor.cpp              | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
index 45785a80d4603..8b34c76965770 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
@@ -106,8 +106,7 @@ PointerFlowEdgeBuilder::addEdges(const DeclPointerLevelVec &LHS,
   return llvm::Error::success();
 }
 
-llvm::Error
-PointerFlowEdgeBuilder::handleRHSAndAddEdges(
+llvm::Error PointerFlowEdgeBuilder::handleRHSAndAddEdges(
     const DeclPointerLevelVec &LHS, const Expr *RHS,
     unsigned ArrayElementIndirectLevel) {
   const auto *ILE = dyn_cast<InitListExpr>(RHS);



More information about the cfe-commits mailing list