[llvm-branch-commits] [clang] [llvm] [SSAF][clang-reforge] Retrofit expressions after CppBoundedBuffers decl rewrites (PR #223105)

Ziqing Luo via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Sat Sep 12 15:15:04 PDT 2026


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

>From 81e4717aeca65241db7f18c24a35f12b192a24eb 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/3] [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 dc7fb830f80daa8c9254b9fab821310a64118966 Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Thu, 10 Sep 2026 14:33:19 -0700
Subject: [PATCH 2/3] [SSAF][PointerFlow] Drop unused TUSummaryExtractor param
 from translateDeclPointerLevel

TUSummaryExtractor is only needed to mint EntityIds when building an
EntityPointerLevel; translateDeclPointerLevel just walks the Expr and
returns raw DeclPointerLevels, so it never touched Extractor.

This is the second patch the radar below depends on:
rdar://187125348
---
 .../EntityPointerLevel/EntityPointerLevel.h   |  5 +--
 .../Analyses/PointerFlow/PointerFlowPairs.h   |  2 +
 .../EntityPointerLevel/EntityPointerLevel.cpp | 44 ++++++++++---------
 .../PointerFlow/PointerFlowExtractor.cpp      |  4 +-
 4 files changed, 30 insertions(+), 25 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h b/clang/include/clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h
index 6a4e4c2879ae1..1a6bf072cb0e1 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h
@@ -110,9 +110,8 @@ translateEntityPointerLevel(const Expr *E, ASTContext &Ctx,
 /// Same as \c translateEntityPointerLevel, except it returns raw
 /// `(NamedDecl *, pointer level, is-return)` tuples (a.k.a. DeclPointerLevels)
 /// instead of assembling an `EntityPointerLevelSet` directly.
-llvm::Expected<DeclPointerLevelVec>
-translateDeclPointerLevel(const Expr *E, ASTContext &Ctx,
-                          TUSummaryExtractor &Extractor);
+llvm::Expected<DeclPointerLevelVec> translateDeclPointerLevel(const Expr *E,
+                                                              ASTContext &Ctx);
 
 /// Assemble `DeclPointerLevels` into an `EntityPointerLevelSet`.
 Expected<EntityPointerLevelSet>
diff --git a/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h b/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
index 06f892a7333cc..5bebbfa5a93c2 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h
@@ -85,6 +85,8 @@ class PointerFlowPairMatcher {
   //   pointer.
   //   - Structured bindings (`auto [a, b] = pair;`) -- the per-element
   //     `BindingDecl`s are neither `VarDecl` nor `FieldDecl`.
+  //   - Compound literals (`(T){...}`) -- not handled, and not covered by
+  //     tests.
 
   /// Match and collect pointer flow.
   /// The macth function 'F' can be described by the following rules:
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.cpp
index 299f1be30dcef..6646cf7364836 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.cpp
@@ -60,7 +60,8 @@ class EntityPointerLevelTranslator
   }
 
   Expected<EntityPointerLevel>
-  createEntityPointerLevelFor(const NamedDecl *ND) {
+  createEntityPointerLevelFor(const NamedDecl *ND,
+                              TUSummaryExtractor &Extractor) {
     std::optional<EntityId> Id = Extractor.addEntity(ND);
     if (!Id)
       return makeErrAtNode(Ctx, ND, "failed to create EntityId for %s",
@@ -69,7 +70,8 @@ class EntityPointerLevelTranslator
   }
 
   Expected<EntityPointerLevel>
-  createEntityPointerLevelForReturn(const FunctionDecl *FD) {
+  createEntityPointerLevelForReturn(const FunctionDecl *FD,
+                                    TUSummaryExtractor &Extractor) {
     std::optional<EntityId> Id = Extractor.addEntityForReturn(FD);
     if (!Id) {
       return makeErrAtNode(Ctx, FD, "failed to create EntityId for function %s",
@@ -91,28 +93,30 @@ class EntityPointerLevelTranslator
     return SubResult;
   }
 
-  TUSummaryExtractor &Extractor;
   ASTContext &Ctx;
 
 public:
-  EntityPointerLevelTranslator(TUSummaryExtractor &Extractor, ASTContext &Ctx)
-      : Extractor(Extractor), Ctx(Ctx) {}
+  EntityPointerLevelTranslator(ASTContext &Ctx) : Ctx(Ctx) {}
 
   Expected<DeclPointerLevelVec> translate(const Expr *E) { return Visit(E); }
-  Expected<EntityPointerLevel> translate(const NamedDecl *D, bool IsRet) {
+  Expected<EntityPointerLevel> translate(const NamedDecl *D, bool IsRet,
+                                         TUSummaryExtractor &Extractor) {
     if (!IsRet)
-      return createEntityPointerLevelFor(D);
+      return createEntityPointerLevelFor(D, Extractor);
 
     if (const auto *FD = dyn_cast<FunctionDecl>(D))
-      return createEntityPointerLevelForReturn(FD);
+      return createEntityPointerLevelForReturn(FD, Extractor);
 
     return makeErrAtNode(Ctx, D, "attempt to get entity for return of %s",
                          D->getDeclKindName());
   }
 
   // Converts a `DeclPointerLevel` to an `EntityPointerLevel`
-  Expected<EntityPointerLevel> toEntityPointerLevel(const DeclPointerLevel &D) {
-    Expected<EntityPointerLevel> Base = translate(D.Decl, D.IsReturn);
+  Expected<EntityPointerLevel>
+  toEntityPointerLevel(const DeclPointerLevel &D,
+                       TUSummaryExtractor &Extractor) {
+    Expected<EntityPointerLevel> Base =
+        translate(D.Decl, D.IsReturn, Extractor);
     if (!Base)
       return Base.takeError();
     return buildEntityPointerLevel(Base->getEntity(), D.PointerLevel);
@@ -334,9 +338,8 @@ class EntityPointerLevelTranslator
 } // namespace clang::ssaf
 
 Expected<DeclPointerLevelVec>
-clang::ssaf::translateDeclPointerLevel(const Expr *E, ASTContext &Ctx,
-                                       TUSummaryExtractor &Extractor) {
-  EntityPointerLevelTranslator Translator(Extractor, Ctx);
+clang::ssaf::translateDeclPointerLevel(const Expr *E, ASTContext &Ctx) {
+  EntityPointerLevelTranslator Translator(Ctx);
 
   return Translator.translate(E);
 }
@@ -344,7 +347,7 @@ clang::ssaf::translateDeclPointerLevel(const Expr *E, ASTContext &Ctx,
 Expected<EntityPointerLevelSet>
 clang::ssaf::translateEntityPointerLevel(const Expr *E, ASTContext &Ctx,
                                          TUSummaryExtractor &Extractor) {
-  EntityPointerLevelTranslator Translator(Extractor, Ctx);
+  EntityPointerLevelTranslator Translator(Ctx);
   auto DPLs = Translator.translate(E);
   if (!DPLs)
     return DPLs.takeError();
@@ -359,9 +362,9 @@ DeclPointerLevel clang::ssaf::createDeclPointerLevel(const NamedDecl *ND,
 /// Create an EntityPointerLevel from a ValueDecl of a pointer type.
 Expected<EntityPointerLevel> clang::ssaf::createEntityPointerLevel(
     const NamedDecl *ND, TUSummaryExtractor &Extractor, bool IsFunRet) {
-  EntityPointerLevelTranslator Translator(Extractor, ND->getASTContext());
+  EntityPointerLevelTranslator Translator(ND->getASTContext());
 
-  return Translator.translate(ND, IsFunRet);
+  return Translator.translate(ND, IsFunRet, Extractor);
 }
 
 DeclPointerLevelVec
@@ -400,11 +403,12 @@ Expected<EntityPointerLevelSet>
 clang::ssaf::toEntityPointerLevels(const DeclPointerLevelVec &DPLs,
                                    ASTContext &Ctx,
                                    TUSummaryExtractor &Extractor) {
-  EntityPointerLevelTranslator Translator(Extractor, Ctx);
+  EntityPointerLevelTranslator Translator(Ctx);
   EntityPointerLevelSet Result;
 
   for (const auto &DPL : DPLs) {
-    Expected<EntityPointerLevel> EPL = Translator.toEntityPointerLevel(DPL);
+    Expected<EntityPointerLevel> EPL =
+        Translator.toEntityPointerLevel(DPL, Extractor);
     if (!EPL)
       return EPL.takeError();
     Result.insert(*EPL);
@@ -415,8 +419,8 @@ clang::ssaf::toEntityPointerLevels(const DeclPointerLevelVec &DPLs,
 Expected<EntityPointerLevel>
 clang::ssaf::toEntityPointerLevel(const DeclPointerLevel &DPL, ASTContext &Ctx,
                                   TUSummaryExtractor &Extractor) {
-  EntityPointerLevelTranslator Translator(Extractor, Ctx);
-  return Translator.toEntityPointerLevel(DPL);
+  EntityPointerLevelTranslator Translator(Ctx);
+  return Translator.toEntityPointerLevel(DPL, Extractor);
 }
 
 EntityPointerLevel clang::ssaf::buildEntityPointerLevel(EntityId Id,
diff --git a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
index 45785a80d4603..24e6423e07ff7 100644
--- a/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowExtractor.cpp
@@ -120,7 +120,7 @@ PointerFlowEdgeBuilder::handleRHSAndAddEdges(
 
     for (DeclPointerLevel &DPL : Copy)
       DPL.PointerLevel += ArrayElementIndirectLevel;
-    return addEdges(Copy, translateDeclPointerLevel(RHS, Ctx, Extractor));
+    return addEdges(Copy, translateDeclPointerLevel(RHS, Ctx));
   }
 
   llvm::Error Err = llvm::Error::success();
@@ -135,7 +135,7 @@ PointerFlowEdgeBuilder::handleRHSAndAddEdges(
 
 llvm::Error PointerFlowEdgeBuilder::operator()(const Expr *LHS,
                                                const Expr *RHS) {
-  auto LVec = translateDeclPointerLevel(LHS, Ctx, Extractor);
+  auto LVec = translateDeclPointerLevel(LHS, Ctx);
   if (!LVec)
     return LVec.takeError();
   return handleRHSAndAddEdges(*LVec, RHS);

>From 0b53ccb2fa2b684bfa6d7f1171e2bf8b220a6da8 Mon Sep 17 00:00:00 2001
From: Ziqing Luo <ziqing_luo at apple.com>
Date: Fri, 11 Sep 2026 16:11:28 -0700
Subject: [PATCH 3/3] [SSAF][clang-reforge] Retrofit expressions after
 CppBoundedBuffers decl rewrites

PR #210457 introduces declaration rewriting.  When CppBoundedBuffers
rewrites a declaration or return type to a bounded_ptr/bounded_array,
some of the existing uses of that entity needs retrofit.

This commit creates expression rewrites for the following patterns:

- Append '.data()' to call arguments when necessary parameter is not
  transformed but argument is transformed.
- Append '.as_bounded<T>()' to call arguments when both parameter and
  argument are transformed but element types are not identical.
- Rewrite '&e[i]' to '(e + i)' and '&*e'/'&(*e)' to 'e', if 'e' is
  transformed.
- Rewrite '(T*)e', 'static_cast<T*>(e)', and 'reinterpret_cast<T*>(e)'
  to 'e.as_bounded<T>()', if 'e' is transformed.

The 3rd step of
rdar://187125348
---
 .../SourceTransformation/CMakeLists.txt       |   9 +
 .../Transformations/CppBoundedBuffers.cpp     | 451 +++++++++++++++++-
 .../CppBoundedBuffersTest.cpp                 | 247 +++++++++-
 .../SourceTransformation/BUILD.gn             |   7 +
 4 files changed, 708 insertions(+), 6 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
index 96e85ec3a50a8..951a40e874398 100644
--- a/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/SourceTransformation/CMakeLists.txt
@@ -2,6 +2,13 @@ set(LLVM_LINK_COMPONENTS
   Support
   )
 
+# TODO: move SSAFAnalysesCommon.h to a higher-up folder shared by both
+# libraries instead of reaching into a sibling library's private header.
+#
+# Needed to reach clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h,
+# a private header shared by the two libraries.
+include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../Analyses)
+
 add_clang_library(clangScalableStaticAnalysisSourceTransformation
   SARIFTransformationReportFormat.cpp
   TransformationRegistry.cpp
@@ -12,6 +19,8 @@ add_clang_library(clangScalableStaticAnalysisSourceTransformation
   clangAST
   clangBasic
   clangLex
+  clangScalableStaticAnalysisAnalyses
   clangScalableStaticAnalysisCore
   clangToolingCore
+  clangToolingRefactoring
   )
diff --git a/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
index c119ee2f01c5e..01c909d5c7597 100644
--- a/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
+++ b/clang/lib/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.cpp
@@ -7,11 +7,14 @@
 //===----------------------------------------------------------------------===//
 
 #include "clang/ScalableStaticAnalysis/SourceTransformation/Transformations/CppBoundedBuffers.h"
+#include "../../Analyses/SSAFAnalysesCommon.h"
 #include "clang/AST/ASTContext.h"
 #include "clang/AST/Decl.h"
 #include "clang/AST/DeclBase.h"
 #include "clang/AST/DeclCXX.h"
 #include "clang/AST/DynamicRecursiveASTVisitor.h"
+#include "clang/AST/Expr.h"
+#include "clang/AST/ExprCXX.h"
 #include "clang/AST/Type.h"
 #include "clang/AST/TypeLoc.h"
 #include "clang/Basic/LangOptions.h"
@@ -20,6 +23,7 @@
 #include "clang/Frontend/SSAFOptions.h"
 #include "clang/Lex/Lexer.h"
 #include "clang/ScalableStaticAnalysis/Analyses/EntityPointerLevel/EntityPointerLevel.h"
+#include "clang/ScalableStaticAnalysis/Analyses/PointerFlow/PointerFlowPairs.h"
 #include "clang/ScalableStaticAnalysis/Analyses/UnsafeBufferUsage/UnsafeBufferUsageAnalysis.h"
 #include "clang/ScalableStaticAnalysis/Core/ASTEntityMapping.h"
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityId.h"
@@ -27,12 +31,18 @@
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationRegistry.h"
 #include "clang/Tooling/Core/Replacement.h"
+#include "clang/Tooling/Refactoring/AtomicChange.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallVector.h"
+#include "llvm/ADT/Twine.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
 #include <cassert>
 #include <map>
 #include <optional>
 #include <string>
+#include <vector>
 
 using namespace clang;
 using namespace clang::ssaf;
@@ -290,6 +300,11 @@ class CollectVisitor : public DynamicRecursiveASTVisitor {
 /// Rewrites or reports every collected declarator and function return.
 class RewriteVisitor : public DynamicRecursiveASTVisitor {
 public:
+  // Decls and their ClassifyResults for all that are successfully
+  // transformed by `emit`:
+  llvm::DenseMap<const Decl *, ClassifyResult> TransformedDecls;
+  llvm::DenseMap<const FunctionDecl *, ClassifyResult> TransformedReturns;
+
   RewriteVisitor(ASTContext &Ctx, DeclLevels &Decls, ReturnLevels &Returns,
                  SourceEditEmitter &Edits, TransformationReportEmitter &Report)
       : Ctx(Ctx), Decls(Decls), Returns(Returns), Edits(Edits), Report(Report) {
@@ -324,8 +339,12 @@ class RewriteVisitor : public DynamicRecursiveASTVisitor {
 
     if (!FunTypeLoc)
       return report(FD, ReportReason::EmissionFailed);
-    return report(FD, emit(FD->getBeginLoc(), NameLoc,
-                           FunTypeLoc.getReturnLoc(), FD->getReturnType(), R));
+
+    auto Reason = emit(FD->getBeginLoc(), NameLoc, FunTypeLoc.getReturnLoc(),
+                       FD->getReturnType(), R);
+    if (!Reason)
+      TransformedReturns[FD] = R;
+    return report(FD, Reason);
   }
 
 private:
@@ -347,7 +366,12 @@ class RewriteVisitor : public DynamicRecursiveASTVisitor {
 
     if (R.Skip)
       return (void)report(D, *R.Skip);
-    report(D, emit(D->getBeginLoc(), NameLoc, TSI->getTypeLoc(), T, R));
+
+    auto Reason = emit(D->getBeginLoc(), NameLoc, TSI->getTypeLoc(), T, R);
+
+    if (!Reason)
+      TransformedDecls[D] = R;
+    report(D, Reason);
   }
 
   /// Compute the precise source range for rewriting.  The produced range is
@@ -482,6 +506,113 @@ class RewriteVisitor : public DynamicRecursiveASTVisitor {
   TransformationReportEmitter &Report;
 };
 
+// FIXME: adding report for any unsuccessful edits
+// FIXME: we need clusters to group edits atomically
+
+/// Traverses the whole TU and create edits for expressions in order to adapt to
+/// transformed Decls.
+class ExpressionRewriter {
+public:
+  ExpressionRewriter(
+      ASTContext &Ctx, const SSAFOptions &Opts, SourceEditEmitter &Edits,
+      const llvm::DenseMap<const Decl *, ClassifyResult> &TransformedDecls,
+      const llvm::DenseMap<const FunctionDecl *, ClassifyResult>
+          &TransformedReturns)
+      : Ctx(Ctx), Opts(Opts), Edits(Edits), TransformedDecls(TransformedDecls),
+        TransformedReturns(TransformedReturns) {}
+
+  /// Traverses the whole \c TU and create edits expressions in order to adapt
+  /// to transformed Decls.
+  void rewriteExprInTU(const TranslationUnitDecl *TU);
+
+  /// Provided pointer/array expression \c E that \c isExprBaseTransformed(E),
+  /// rewrite the expression if necessary to produce correct bounded type
+  /// expression.
+  ///
+  /// \return true iff a rewrite edit was created.
+  bool rewriteExpression(const Expr *E, tooling::AtomicChange &AC) const;
+
+  /// Adapt expressions of a pointer flow to Decl transformations.
+  /// \return \c tooling::AtomicChange if any edit was created.
+  std::optional<tooling::AtomicChange>
+  adaptPointerFlow(const PointerFlowPair &Pair) const;
+
+private:
+  /// Associate edit operations to \c CharSourceRange always, since they carry
+  /// token/char range info while single SourceLocation doesn't.  This way, the
+  /// edit kind (replacement or insertion) cannot be inferred from the input
+  /// (i.e., SourceRange vs. SourceLocation). Use this enum to explicitly
+  /// express the kinds.
+  enum EditKind { Replace, InsertAtBegin, InsertAtEnd };
+
+  /// \param Range The source locations associated with the edit. For any \c
+  /// EditKind, source locations are given by a \c CharSourceRange, which
+  /// carries the information of whether it is a token range or a char range.
+  /// \param NewText The text of the edit that will replace a source range or
+  /// be inserted at a location.
+  /// \param EditKind The kind of edit: replacement or insertion.
+  /// \param AC IN/OUT parameter. The new edit will be added to \c AC
+  /// \return true if edit is successfully added to \c AC
+  bool addEditToAtomicChange(CharSourceRange Range, StringRef NewText,
+                             EditKind EditKind,
+                             tooling::AtomicChange &AC) const;
+
+  /// \return true iff the base(s) of the pointer/array expression \c E  are all
+  /// transformed to have bounded types.
+  bool isExprBaseTransformed(const Expr *E) const;
+
+  /// \return a non-null pointer to a \c ClassifyResult, if `D` is transformed
+  /// to have bounded types.
+  const ClassifyResult *getDeclClassifyResultsIfTransformed(const Decl *D,
+                                                            bool IsRet) const;
+
+  /// \return a non-empty vector of \c ClassifyResult, the base(s) of the
+  /// pointer/array expression \c E  are all transformed to have bounded
+  /// types.
+  std::vector<const ClassifyResult *>
+  getPtrExprClassifyResultsIfTransformed(const Expr *E) const;
+
+  friend struct GetLHSClassifyResults;
+
+  //==----------------  Expression rewrite rules  -------------------==//
+
+  /// Provided that \c Pair LHS is NOT transformed but RHS is transformed,
+  /// wrap RHS in '(RHS).data()' if appropriate.
+  /// \return true iff RHS needs edit and \c addEditToAtomicChange is called on
+  /// AC.
+  bool appendDataCallToArg(const PointerFlowPair &Pair,
+                           tooling::AtomicChange &AC) const;
+
+  /// Provided that \c Pair LHS and RHS are both transformed,
+  /// wrap RHS in '(RHS).as_bounded<T>()', if their element types are not
+  /// identical.
+  /// \return true iff RHS needs edit and \c addEditToAtomicChange is called on
+  /// AC.
+  bool appendAsBoundedCallToArg(const PointerFlowPair &Pair,
+                                tooling::AtomicChange &AC) const;
+
+  /// Provided \c E is transformed, if it has the form '&e[i]' or '&*e',
+  /// edit it to '(e + i)' or 'e', resp.
+  /// \return true iff E needs edit and \c addEditToAtomicChange is called on
+  /// AC.
+  bool rewriteAddrofElementAccess(const Expr *E,
+                                   tooling::AtomicChange &AC) const;
+
+  /// Provided \c E is transformed, if it has the form
+  /// '(T*)e/static_cast<T>(e)/reinterpret_cast<T>(e)', edit it to
+  /// '(e).as_bounded<T>()'.
+  /// \return true iff E needs edit and \c addEditToAtomicChange is called on
+  /// AC.
+  bool rewritePointerCast(const Expr *E, tooling::AtomicChange &AC) const;
+
+  ASTContext &Ctx;
+  const SSAFOptions &Opts;
+  SourceEditEmitter &Edits;
+  const llvm::DenseMap<const Decl *, ClassifyResult> &TransformedDecls;
+  const llvm::DenseMap<const FunctionDecl *, ClassifyResult>
+      &TransformedReturns;
+};
+
 } // namespace
 
 namespace clang::ssaf {
@@ -607,14 +738,324 @@ void CppBoundedBuffers::HandleTranslationUnit(ASTContext &Ctx) {
   DeclLevels Decls;
   ReturnLevels Returns;
 
-  Decl *TU = Ctx.getTranslationUnitDecl();
+  auto *TU = Ctx.getTranslationUnitDecl();
   CollectVisitor(Reach, TUNamespace, LUNamespace, Decls, Returns)
       .TraverseDecl(TU);
-  RewriteVisitor(Ctx, Decls, Returns, Edits, Report).TraverseDecl(TU);
+  auto RV = RewriteVisitor(Ctx, Decls, Returns, Edits, Report);
+
+  RV.TraverseDecl(TU);
+
+  ExpressionRewriter ExprRewriter(Ctx, Opts, Edits, RV.TransformedDecls,
+                                  RV.TransformedReturns);
+
+  ExprRewriter.rewriteExprInTU(TU);
 }
 
 } // namespace clang::ssaf
 
+namespace {
+
+//===------------ ExpressionRewriter implementation --------------===//
+struct GetLHSClassifyResults {
+  const ExpressionRewriter &ER;
+
+  std::vector<const ClassifyResult *> operator()(const ValueDecl *D,
+                                                 bool IsRet) const {
+    if (const ClassifyResult *R =
+            ER.getDeclClassifyResultsIfTransformed(D, IsRet))
+      return {R};
+    return {};
+  }
+
+  std::vector<const ClassifyResult *> operator()(const Expr *E) const {
+    return ER.getPtrExprClassifyResultsIfTransformed(E);
+  }
+};
+
+std::optional<tooling::AtomicChange>
+ExpressionRewriter::adaptPointerFlow(const PointerFlowPair &Pair) const {
+  auto LHSClassifyResults = Pair.visitLHS(GetLHSClassifyResults{*this});
+  bool IsLHSTransformed = !LHSClassifyResults.empty();
+  bool IsRHSTransformed = isExprBaseTransformed(Pair.RHS);
+  tooling::AtomicChange AC("", "");
+
+  if (IsRHSTransformed)
+    rewriteExpression(Pair.RHS, AC);
+  if (!IsLHSTransformed && IsRHSTransformed)
+    appendDataCallToArg(Pair, AC);
+  if (IsLHSTransformed && IsRHSTransformed)
+    appendAsBoundedCallToArg(Pair, AC);
+  return AC;
+}
+
+bool ExpressionRewriter::rewriteExpression(const Expr *E,
+                                           tooling::AtomicChange &AC) const {
+  return rewriteAddrofElementAccess(E, AC) || rewritePointerCast(E, AC);
+}
+
+bool ExpressionRewriter::appendDataCallToArg(const PointerFlowPair &Pair,
+                                             tooling::AtomicChange &AC) const {
+  const auto *PVD =
+      dyn_cast_or_null<ParmVarDecl>(Pair.LHS.dyn_cast<const ValueDecl *>());
+
+  if (!PVD)
+    return false;
+
+  // '(RHS).data()' is a prvalue that can't bind to a reference. So bail.
+  if (PVD->getType()->isReferenceType())
+    return false;
+
+  QualType RTypeBeforeImpCast = Pair.RHS->IgnoreImpCasts()->getType();
+  QualType LType = PVD->getType().getNonReferenceType();
+
+  // If RHS has `void*` type, it will have `char*` after transformation and
+  // being appened '.data()'.  This type change may cause the callee to be
+  // silently swapped to a different overload. So bail.
+  if (RTypeBeforeImpCast->isVoidPointerType() && LType->isVoidPointerType())
+    return false;
+
+  CharSourceRange RHSRange = Lexer::getAsCharRange(
+      Pair.RHS->getSourceRange(), Ctx.getSourceManager(), Ctx.getLangOpts());
+
+  addEditToAtomicChange(RHSRange, "(", EditKind::InsertAtBegin, AC);
+  addEditToAtomicChange(RHSRange, ").data()", EditKind::InsertAtEnd, AC);
+  return true;
+}
+
+bool ExpressionRewriter::appendAsBoundedCallToArg(
+    const PointerFlowPair &Pair, tooling::AtomicChange &AC) const {
+  const auto *PVD =
+      dyn_cast_or_null<ParmVarDecl>(Pair.LHS.dyn_cast<const ValueDecl *>());
+
+  if (!PVD)
+    return false;
+
+  CharSourceRange RHSRange = Lexer::getAsCharRange(
+      Pair.RHS->getSourceRange(), Ctx.getSourceManager(), Ctx.getLangOpts());
+  QualType RTypeBeforeImpCast = Pair.RHS->IgnoreImpCasts()->getType();
+  QualType LType = PVD->getType().getNonReferenceType();
+  QualType RPteTy = RTypeBeforeImpCast->getPointeeType();
+  QualType LPteTy = LType->getPointeeType();
+
+  if (LPteTy.isNull() || RPteTy.isNull())
+    return false;
+
+  if (Ctx.hasSameType(LPteTy, RPteTy))
+    return false;
+
+  auto *LHSClassifyResult = getDeclClassifyResultsIfTransformed(PVD, false);
+
+  if (!LHSClassifyResult)
+    return false;
+
+  StringRef LHSInnerSpelling = LHSClassifyResult->InnerSpelling;
+
+  addEditToAtomicChange(RHSRange, "(", EditKind::InsertAtBegin, AC);
+  addEditToAtomicChange(
+      RHSRange,
+      (").as_bounded<" + LHSInnerSpelling + ">()").getSingleStringRef(),
+      EditKind::InsertAtEnd, AC);
+  return true;
+}
+
+bool ExpressionRewriter::rewriteAddrofElementAccess(
+    const Expr *E, tooling::AtomicChange &AC) const {
+  const auto *UO = dyn_cast<UnaryOperator>(E->IgnoreParenImpCasts());
+  if (!UO || UO->getOpcode() != UO_AddrOf)
+    return false;
+
+  const Expr *SubExpr = UO->getSubExpr()->IgnoreParenImpCasts();
+  const Expr *Ptr, *Offset = nullptr;
+
+  if (const auto *ASE = dyn_cast<ArraySubscriptExpr>(SubExpr)) {
+    Ptr = ASE->getBase();
+    Offset = ASE->getIdx();
+  } else if (const auto *Deref = dyn_cast<UnaryOperator>(SubExpr);
+             Deref && Deref->getOpcode() == UO_Deref) {
+    Ptr = Deref->getSubExpr();
+  } else
+    return false;
+
+  rewriteExpression(Ptr, AC);
+  // Ptr may have been recursively edited, so its source range should stay
+  // intact.
+
+  const SourceManager &SM = Ctx.getSourceManager();
+  const LangOptions &LO = Ctx.getLangOpts();
+  auto PtrCR = Lexer::getAsCharRange(Ptr->getSourceRange(), SM, LO);
+  auto FullExprCR = Lexer::getAsCharRange(UO->getSourceRange(), SM, LO);
+  // Source range before `Ptr`:
+  auto PrePtrCR =
+      CharSourceRange::getCharRange(FullExprCR.getBegin(), PtrCR.getBegin());
+
+  if (!Offset) {
+    // Source range after `Ptr`:
+    auto PostPtrCR =
+        CharSourceRange::getCharRange(PtrCR.getEnd(), FullExprCR.getEnd());
+    // For '&*ptr' or '&(*ptr)', drop contents in PrePtrCR and PostPtrCR:
+    addEditToAtomicChange(PrePtrCR, "", EditKind::Replace, AC);
+    addEditToAtomicChange(PostPtrCR, "", EditKind::Replace, AC);
+    return true;
+  }
+
+  auto OffsetCR = Lexer::getAsCharRange(Offset->getSourceRange(), SM, LO);
+  auto PostPtrPreOffsetCR =
+      CharSourceRange::getCharRange(PtrCR.getEnd(), OffsetCR.getBegin());
+  auto PostOffsetCR =
+      CharSourceRange::getCharRange(OffsetCR.getEnd(), FullExprCR.getEnd());
+
+  // For '&ptr[offset]' or '&(ptr[offset])',
+  // 1. replace contents in PrePtrCR with "(", and
+  // 2. replace contents in PostPtrPreOffsetCR with " + ", and
+  // 3. replace contents in postOffsetCR  with ") ",
+  // results in '(ptr + offset)':
+  addEditToAtomicChange(PrePtrCR, "(", EditKind::Replace, AC);
+  addEditToAtomicChange(PostPtrPreOffsetCR, " + ", EditKind::Replace, AC);
+  addEditToAtomicChange(PostOffsetCR, ")", EditKind::Replace, AC);
+  return true;
+}
+
+bool ExpressionRewriter::rewritePointerCast(const Expr *E,
+                                             tooling::AtomicChange &AC) const {
+  const auto *CE = dyn_cast<ExplicitCastExpr>(E->IgnoreParenImpCasts());
+
+  if (!CE ||
+      !isa<CStyleCastExpr, CXXStaticCastExpr, CXXReinterpretCastExpr>(CE))
+    return false;
+
+  QualType DestTy = CE->getTypeAsWritten();
+  QualType DestPteTy = DestTy->getPointeeType();
+
+  if (!DestTy->isPointerType())
+    return false;
+
+  const Expr *Ptr = CE->getSubExpr();
+  rewriteExpression(Ptr, AC);
+  // Ptr may have been recursively edited, so its source range should stay
+  // intact.
+
+  const SourceManager &SM = Ctx.getSourceManager();
+  const LangOptions &LO = Ctx.getLangOpts();
+  CharSourceRange PtrCR =
+      Lexer::getAsCharRange(CE->getSubExpr()->getSourceRange(), SM, LO);
+  CharSourceRange FullCastExprCR =
+      Lexer::getAsCharRange(CE->getSourceRange(), SM, LO);
+  CharSourceRange PrePtrCR = CharSourceRange::getCharRange(
+      FullCastExprCR.getBegin(), PtrCR.getBegin());
+  CharSourceRange PostPtrCR =
+      CharSourceRange::getCharRange(PtrCR.getEnd(), FullCastExprCR.getEnd());
+  std::string T = spell(DestPteTy, Ctx);
+
+  // For `(T*)ptr` or *_cast<T>(ptr),
+  // 1. replace contents in PrePtrCR with "(", and
+  // 2. replace contents in PostPtrCR with ").as_bounded<T>()",
+  // results in '(ptr).as_bounded<T>()'.
+  addEditToAtomicChange(PrePtrCR, "(", EditKind::Replace, AC);
+  addEditToAtomicChange(PostPtrCR, ").as_bounded<" + T + ">()",
+                        EditKind::Replace, AC);
+  return true;
+}
+
+void ExpressionRewriter::rewriteExprInTU(const TranslationUnitDecl *TU) {
+  llvm::DenseMap<const NamedDecl *, std::vector<const NamedDecl *>>
+      ContributorGroups;
+
+  findContributors(Ctx, Opts, ContributorGroups,
+                   /*ExtractFromSystemHeaders=*/false);
+
+  llvm::SmallVector<PointerFlowPair> Pairs;
+  PointerFlowPairMatcher Matcher{Ctx};
+
+  for (auto &[GrpCano, ContriGrp] : ContributorGroups)
+    for (auto *ContriDecl : ContriGrp) {
+      auto PairsCollector = [&Pairs, &Matcher,
+                             &ContriDecl](const DynTypedNode &Node) {
+        Matcher.matches(Node, ContriDecl, Pairs);
+      };
+      findMatchesIn(ContriDecl, PairsCollector);
+    }
+
+  for (const PointerFlowPair &Pair : Pairs) {
+    if (auto AC = adaptPointerFlow(Pair);
+        AC && llvm::all_of(AC->getReplacements(),
+                           std::mem_fn(&tooling::Replacement::isApplicable)))
+      for (const tooling::Replacement &R : AC->getReplacements())
+        Edits.addReplacement(R);
+  }
+}
+
+bool ExpressionRewriter::isExprBaseTransformed(const Expr *E) const {
+  return !getPtrExprClassifyResultsIfTransformed(E).empty();
+}
+
+const ClassifyResult *
+ExpressionRewriter::getDeclClassifyResultsIfTransformed(const Decl *D,
+                                                        bool IsRet) const {
+  auto Lookup = [](const auto &Map, const auto *Key) {
+    auto It = Map.find(Key);
+    return It == Map.end() ? nullptr : &It->second;
+  };
+  return IsRet ? Lookup(TransformedReturns, cast<FunctionDecl>(D))
+               : Lookup(TransformedDecls, D);
+}
+
+std::vector<const ClassifyResult *>
+ExpressionRewriter::getPtrExprClassifyResultsIfTransformed(
+    const Expr *E) const {
+  auto DPLs = translateDeclPointerLevel(E, Ctx);
+
+  if (!DPLs) {
+    // Errors indicate no transformation for E. No further action.
+    llvm::consumeError(DPLs.takeError());
+    return {};
+  }
+
+  std::vector<const ClassifyResult *> Result;
+
+  for (auto &DPL : *DPLs) {
+    const auto *ClassifyResult =
+        getDeclClassifyResultsIfTransformed(DPL.Decl, DPL.IsReturn);
+
+    if (!ClassifyResult)
+      return {};
+    Result.push_back(ClassifyResult);
+  }
+  return Result;
+}
+
+bool ExpressionRewriter::addEditToAtomicChange(
+    CharSourceRange Range, StringRef NewText, EditKind EditKind,
+    tooling::AtomicChange &AC) const {
+  assert(Range.isCharRange());
+
+  if (Range.getBegin().isMacroID() || Range.getEnd().isMacroID())
+    // FIXME: report...
+    return false;
+
+  const SourceManager &SM = Ctx.getSourceManager();
+  llvm::Error Err = [&]() -> llvm::Error {
+    switch (EditKind) {
+    case Replace:
+      return AC.replace(SM, Range, NewText);
+    case InsertAtBegin:
+      return AC.insert(SM, Range.getBegin(), NewText, /*InsertAfter=*/false);
+    case InsertAtEnd:
+      return AC.insert(SM, Range.getEnd(), NewText, /*InsertAfter=*/true);
+    }
+    llvm_unreachable("unhandled EditKind");
+  }();
+
+  if (Err) {
+    llvm::consumeError(std::move(Err));
+    // FIXME: generate a Report
+    // If AtomicChange has an error,the whole should be discard
+    return false;
+  }
+  return true;
+}
+
+} // namespace
+
 namespace clang::ssaf {
 // NOLINTNEXTLINE(misc-use-internal-linkage)
 volatile int CppBoundedBuffersAnchorSource = 0;
diff --git a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
index 54fc08b6e8ab7..4d0ba31cc36ed 100644
--- a/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/SourceTransformation/CppBoundedBuffersTest.cpp
@@ -24,7 +24,6 @@
 #include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationReportEmitter.h"
 #include "clang/Tooling/Core/Replacement.h"
 #include "clang/Tooling/Tooling.h"
-#include "llvm/ADT/ArrayRef.h"
 #include "llvm/ADT/SmallSet.h"
 #include "llvm/ADT/StringSet.h"
 #include "llvm/Support/Error.h"
@@ -687,4 +686,250 @@ TEST_F(CppBoundedBuffersTest, MessageForIsUnique) {
   }
 }
 
+//===----------------------------------------------------------------------===//
+// Expression rewriting tests
+//===----------------------------------------------------------------------===//
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataDRE) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int *q; f(q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(int *p);
+    void g() { bounded_ptr<int> q; f((q).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataAddrOfDeref) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int *q; f(&*q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(int *p);
+    void g() { bounded_ptr<int> q; f((q).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataAddrOfDerefParen) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int *q; f(&(*q)); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(int *p);
+    void g() { bounded_ptr<int> q; f((q).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataAddrOfSubscript) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g(int i) { int arr[4]; f(&arr[i]); }
+  )cpp";
+  Captured C =
+      run(Code, [](ASTContext &Ctx) { return varEntity("arr", Ctx); }, {1});
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(int *p);
+    void g(int i) { bounded_array<int, 4> arr; f(((arr + i)).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataAddrOfSubscript2) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g(int i) { int *q; f(&q[i]); }
+  )cpp";
+  Captured C =
+      run(Code, [](ASTContext &Ctx) { return varEntity("q", Ctx); }, {1});
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(int *p);
+    void g(int i) { bounded_ptr<int> q; f(((q + i)).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataPtrCast) {
+  StringRef Code = R"cpp(
+    void f(char *p);
+    void g() { int *q; f((char*)q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(char *p);
+    void g() { bounded_ptr<int> q; f(((q).as_bounded<char>()).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataPtrCastStaticCast) {
+  StringRef Code = R"cpp(
+    void f(char *p);
+    void g() { int *q; f(reinterpret_cast<char*>(q)); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(char *p);
+    void g() { bounded_ptr<int> q; f(((q).as_bounded<char>()).data()); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgAddDataPtrCastAddrOfSubscript) {
+  StringRef Code = R"cpp(
+    void f(char *p);
+    void g(int i) {
+      int *q;
+      f((char*)&q[i]);
+    }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(char *p);
+    void g(int i) {
+      bounded_ptr<int> q;
+      f((((q + i)).as_bounded<char>()).data());
+    }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, AssignPtrCastAddrOfSubscript) {
+  // The argument is a C-style cast of an address-of-subscript, '(char*)&q[i]'.
+  // 'q' is transformed but f's parameter is not.
+  StringRef Code = R"cpp(
+    void g(int i) {
+      int *q;
+      char *p = (char*)&q[i];
+    }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void g(int i) {
+      bounded_ptr<int> q;
+      char *p = ((q + i)).as_bounded<char>();
+    }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgNoChangeImplicit) {
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int *q; f(q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, paramEntity("f", 0, Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(bounded_ptr<int> p);
+    void g() { int *q; f(q); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgNoChangeBothHardened) {
+  // Both the parameter and the argument are rewritten to the same bounded
+  // type, so the call site needs no adjustment.
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int *q; f(q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, paramEntity("f", 0, Ctx), {1});
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(bounded_ptr<int> p);
+    void g() { bounded_ptr<int> q; f(q); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgNoChangeVoidParamVoidArg) {
+  // 'q' is void*, so it gets rewritten to 'bounded_ptr<char>', not
+  // 'bounded_ptr<void>'.  If '.data()' were appended it would return
+  // 'char *', which could silently steal a different overload of 'f' than
+  // the original 'void *' argument would have. So bail rather than adding
+  // '.data()' when both the parameter and the argument's original type are
+  // 'void*'.
+  StringRef Code = R"cpp(
+    void f(void *p);
+    void g() { void *q; f(q); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, varEntity("q", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(void *p);
+    void g() { bounded_ptr<char> q; f(q); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, CallArgNoChangeArrayDecayMatchingType) {
+  // 'arr' is passed directly (no cast), so its element type must already
+  // match 'f's parameter pointee type pre-transformation. The implicit
+  // 'bounded_array<T, N>' -> 'bounded_ptr<T>' conversion bridges this case,
+  // so no '.as_bounded<T>()' is needed at the call site.
+  StringRef Code = R"cpp(
+    void f(int *p);
+    void g() { int arr[4]; f(arr); }
+  )cpp";
+  Captured C = runMarked(Code, [](ASTContext &Ctx, WPASuite &Suite,
+                                  UnsafeBufferReachableAnalysisResult &Result) {
+    markReachable(Suite, Result, paramEntity("f", 0, Ctx), {1});
+    markReachable(Suite, Result, varEntity("arr", Ctx), {1});
+  });
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void f(bounded_ptr<int> p);
+    void g() { bounded_array<int, 4> arr; f(arr); }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
+TEST_F(CppBoundedBuffersTest, RHSNoChangeRHSNotHardened) {
+  StringRef Code = R"cpp(
+    void g() { int arr[4]; int *p = &arr[0]; }
+  )cpp";
+  Captured C =
+      run(Code, [](ASTContext &Ctx) { return varEntity("p", Ctx); }, {1});
+  EXPECT_EQ(C.Rewritten, R"cpp(
+    void g() { int arr[4]; bounded_ptr<int> p = &arr[0]; }
+  )cpp");
+  EXPECT_TRUE(C.Reports.empty());
+}
+
 } // namespace
diff --git a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/SourceTransformation/BUILD.gn b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/SourceTransformation/BUILD.gn
index e6e1af033db8a..0ffac2699ffae 100644
--- a/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/SourceTransformation/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/lib/ScalableStaticAnalysis/SourceTransformation/BUILD.gn
@@ -1,11 +1,18 @@
 static_library("SourceTransformation") {
   output_name = "clangScalableStaticAnalysisSourceTransformation"
   configs += [ "//llvm/utils/gn/build:clang_code" ]
+
+  # Needed to reach
+  # clang/lib/ScalableStaticAnalysis/Analyses/SSAFAnalysesCommon.h, a private
+  # header shared by the two libraries.
+  include_dirs = [ "//clang/lib/ScalableStaticAnalysis/Analyses" ]
   deps = [
     "//clang/lib/AST",
     "//clang/lib/Basic",
+    "//clang/lib/ScalableStaticAnalysis/Analyses",
     "//clang/lib/ScalableStaticAnalysis/Core",
     "//clang/lib/Tooling/Core",
+    "//clang/lib/Tooling/Refactoring",
     "//llvm/lib/Support",
   ]
   sources = [



More information about the llvm-branch-commits mailing list