[clang] [clang][ssaf] Add JSONFormat support for WPASuite (PR #187403)

Aviral Goel via cfe-commits cfe-commits at lists.llvm.org
Thu Mar 26 07:35:41 PDT 2026


https://github.com/aviralg updated https://github.com/llvm/llvm-project/pull/187403

>From 397d5649770f59907fb910c6be127d322b9fdd6a Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Sat, 14 Mar 2026 09:23:07 -0700
Subject: [PATCH 01/30] Analysis Tree

---
 .../Core/Analysis/AnalysisBase.h              |  55 +++
 .../Core/Analysis/AnalysisDriver.h            |  91 ++++
 .../Core/Analysis/AnalysisRegistry.h          |  95 +++++
 .../Core/Analysis/AnalysisResult.h            |  30 ++
 .../Core/Analysis/DerivedAnalysis.h           | 133 ++++++
 .../Core/Analysis/SummaryAnalysis.h           | 132 ++++++
 .../Core/Analysis/WPASuite.h                  |  92 +++++
 .../Core/EntityLinker/LUSummary.h             |   1 +
 .../Core/Model/AnalysisName.h                 |  49 +++
 .../Core/Model/AnalysisTraits.h               |  33 ++
 .../Core/Support/FormatProviders.h            |   8 +
 .../Core/Analysis/AnalysisDriver.cpp          | 196 +++++++++
 .../Core/Analysis/AnalysisRegistry.cpp        |  37 ++
 .../Core/CMakeLists.txt                       |   3 +
 .../Core/Model/AnalysisName.cpp               |  16 +
 .../Analysis/AnalysisDriverTest.cpp           | 390 ++++++++++++++++++
 .../CMakeLists.txt                            |   1 +
 17 files changed, 1362 insertions(+)
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h
 create mode 100644 clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp
 create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
new file mode 100644
index 0000000000000..478a6fa85d4a8
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
@@ -0,0 +1,55 @@
+//===- AnalysisBase.h -----------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Minimal common base for SummaryAnalysisBase and DerivedAnalysisBase.
+// Carries the identity (analysisName()) and dependency list
+// (dependencyNames()) shared by every analysis regardless of kind.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include <vector>
+
+namespace clang::ssaf {
+
+class AnalysisDriver;
+class SummaryAnalysisBase;
+class DerivedAnalysisBase;
+
+/// Minimal common base for both analysis kinds.
+///
+/// Not subclassed directly — use SummaryAnalysis<...> or
+/// DerivedAnalysis<...> instead.
+class AnalysisBase {
+  friend class AnalysisDriver;
+  friend class SummaryAnalysisBase;
+  friend class DerivedAnalysisBase;
+
+  enum class Kind { Summary, Derived };
+  Kind TheKind;
+
+protected:
+  explicit AnalysisBase(Kind K) : TheKind(K) {}
+
+public:
+  virtual ~AnalysisBase() = default;
+
+  /// Name of this analysis. Equal to ResultT::analysisName() in both typed
+  /// intermediates.
+  virtual AnalysisName analysisName() const = 0;
+
+  /// AnalysisNames of all AnalysisResult dependencies.
+  virtual const std::vector<AnalysisName> &dependencyNames() const = 0;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
new file mode 100644
index 0000000000000..1cc5c18d348b9
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
@@ -0,0 +1,91 @@
+//===- AnalysisDriver.h ---------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Central orchestrator for whole-program analysis. Takes ownership of an
+// LUSummary, drives all registered analyses in topological dependency order,
+// and returns a WPASuite.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/Support/Error.h"
+#include <memory>
+#include <vector>
+
+namespace clang::ssaf {
+
+/// Orchestrates whole-program analysis over an LUSummary.
+///
+/// Three run() patterns are supported:
+///   - run() &&        — all registered analyses; silently skips any whose
+///                       entity data is absent or whose dependency was skipped.
+///                       Requires an rvalue driver because this exhausts the
+///                       LUSummary.
+///   - run(names)      — named subset plus transitive dependencies; returns
+///                       Expected and fails if any listed name has no
+///                       registered analysis or missing entity data.
+///   - run<ResultTs..> — type-safe variant of run(names).
+class AnalysisDriver final {
+public:
+  explicit AnalysisDriver(std::unique_ptr<LUSummary> LU);
+
+  /// Runs all registered analyses in topological dependency order.
+  /// Silently skips analyses with absent entity data or skipped dependencies.
+  ///
+  /// Requires an rvalue driver (std::move(Driver).run()) because this
+  /// exhausts all remaining LUSummary data.
+  [[nodiscard]] llvm::Expected<WPASuite> run() &&;
+
+  /// Runs only the named analyses (plus their transitive dependencies).
+  ///
+  /// Returns an error if any listed AnalysisName has no registered analysis
+  /// or if a required SummaryAnalysis has no matching entity data in the
+  /// LUSummary. The EntityIdTable is copied (not moved) so the driver remains
+  /// usable for subsequent calls.
+  [[nodiscard]] llvm::Expected<WPASuite>
+  run(llvm::ArrayRef<AnalysisName> Names);
+
+  /// Type-safe variant of run(names). Derives names from
+  /// ResultTs::analysisName().
+  template <typename... ResultTs> [[nodiscard]] llvm::Expected<WPASuite> run() {
+    return run({ResultTs::analysisName()...});
+  }
+
+private:
+  std::unique_ptr<LUSummary> LU;
+
+  /// Instantiates all analyses reachable from \p Roots (plus transitive
+  /// dependencies) and returns them in topological order via a single DFS.
+  /// Reports an error on unregistered names or cycles.
+  static llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
+  sortTopologically(llvm::ArrayRef<AnalysisName> Roots);
+
+  /// Executes a topologically-sorted analysis list and returns a WPASuite.
+  /// \p IdTable is moved into the returned WPASuite.
+  llvm::Expected<WPASuite>
+  execute(EntityIdTable IdTable,
+          std::vector<std::unique_ptr<AnalysisBase>> Sorted);
+
+  llvm::Error
+  executeSummaryAnalysis(std::unique_ptr<SummaryAnalysisBase> Summary,
+                         WPASuite &Suite);
+
+  llvm::Error
+  executeDerivedAnalysis(std::unique_ptr<DerivedAnalysisBase> Derived,
+                         WPASuite &Suite);
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
new file mode 100644
index 0000000000000..e4faba62c070e
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -0,0 +1,95 @@
+//===- AnalysisRegistry.h -------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Unified registry for both SummaryAnalysis and DerivedAnalysis subclasses.
+//
+// To register an analysis, add a static Add<AnalysisT> in its translation
+// unit:
+//
+//   static AnalysisRegistry::Add<MyAnalysis>
+//       Registered("One-line description of MyAnalysis");
+//
+// The registry entry name is derived automatically from
+// MyAnalysis::analysisName(), so name-mismatch bugs are impossible.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "llvm/Support/Registry.h"
+#include <memory>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace clang::ssaf {
+
+/// Unified registry for SummaryAnalysis and DerivedAnalysis implementations.
+///
+/// Internally uses a single llvm::Registry<AnalysisBase>. The correct kind
+/// is carried by the AnalysisBase::TheKind tag set in each subclass
+/// constructor.
+class AnalysisRegistry {
+  using RegistryT = llvm::Registry<AnalysisBase>;
+
+  AnalysisRegistry() = delete;
+
+public:
+  /// Registers AnalysisT with the unified registry.
+  ///
+  /// The registry entry name is derived automatically from
+  /// AnalysisT::ResultType::analysisName(), so name-mismatch bugs are
+  /// impossible.
+  ///
+  /// Add objects must be declared static at namespace scope.
+  template <typename AnalysisT> struct Add {
+    static_assert(std::is_base_of_v<SummaryAnalysisBase, AnalysisT> ||
+                      std::is_base_of_v<DerivedAnalysisBase, AnalysisT>,
+                  "AnalysisT must derive from SummaryAnalysis<...> or "
+                  "DerivedAnalysis<...>");
+
+    explicit Add(llvm::StringRef Desc)
+        : Name(AnalysisT::ResultType::analysisName().str().str()),
+          Node(Name, Desc) {
+      if (contains(Name)) {
+        ErrorBuilder::fatal("duplicate analysis registration for '{0}'", Name);
+      }
+      analysisNames.push_back(AnalysisT::ResultType::analysisName());
+    }
+
+    Add(const Add &) = delete;
+    Add &operator=(const Add &) = delete;
+
+  private:
+    std::string Name;
+    RegistryT::Add<AnalysisT> Node;
+  };
+
+  /// Returns true if an analysis is registered under \p Name.
+  static bool contains(llvm::StringRef Name);
+
+  /// Returns the names of all registered analyses.
+  static const std::vector<AnalysisName> &names();
+
+  /// Instantiates the analysis registered under \p Name, or returns
+  /// std::nullopt if no such analysis is registered.
+  static std::optional<std::unique_ptr<AnalysisBase>>
+  instantiate(llvm::StringRef Name);
+
+private:
+  static std::vector<AnalysisName> analysisNames;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
new file mode 100644
index 0000000000000..87d781cff30a8
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
@@ -0,0 +1,30 @@
+//===- AnalysisResult.h ---------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Base class for all whole-program analysis results produced by AnalysisDriver.
+// Replaces SummaryData. Concrete subclasses carry a static analysisName().
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
+
+namespace clang::ssaf {
+
+/// Base class for whole-program analysis results.
+///
+/// Concrete subclasses must provide:
+///   static AnalysisName analysisName();
+class AnalysisResult {
+public:
+  virtual ~AnalysisResult() = default;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
new file mode 100644
index 0000000000000..7b5695dab7c72
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -0,0 +1,133 @@
+//===- DerivedAnalysis.h --------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Defines DerivedAnalysisBase (type-erased base known to AnalysisDriver) and
+// the typed intermediate DerivedAnalysis<ResultT, DepResultTs...> that
+// concrete analyses inherit from.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "llvm/Support/Error.h"
+#include <map>
+#include <memory>
+#include <vector>
+
+namespace clang::ssaf {
+
+class AnalysisDriver;
+
+/// Type-erased base for derived analyses. Known to AnalysisDriver.
+///
+/// Not subclassed directly — use DerivedAnalysis<ResultT, DepResultTs...>.
+/// A derived analysis consumes previously produced AnalysisResult objects
+/// and computes a new one via an initialize/step/finalize lifecycle.
+class DerivedAnalysisBase : public AnalysisBase {
+  friend class AnalysisDriver;
+
+protected:
+  DerivedAnalysisBase() : AnalysisBase(AnalysisBase::Kind::Derived) {}
+
+private:
+  /// Called once with the dependency results before the step() loop.
+  ///
+  /// \param DepResults  Immutable results of all declared dependencies, keyed
+  ///                    by AnalysisName. Guaranteed to contain every name
+  ///                    returned by dependencyNames().
+  virtual llvm::Error initialize(
+      const std::map<AnalysisName, const AnalysisResult *> &DepResults) = 0;
+
+  /// Performs one pass. Returns true if another pass is needed; false when
+  /// converged.
+  virtual llvm::Expected<bool> step() = 0;
+
+  /// Called after the step() loop converges. Default is a no-op.
+  virtual llvm::Error finalize() { return llvm::Error::success(); }
+
+  /// Transfers ownership of the computed result. Called once after finalize().
+  virtual std::unique_ptr<AnalysisResult> result() && = 0;
+};
+
+/// Typed intermediate that concrete derived analyses inherit from.
+///
+/// Concrete analyses must implement:
+///   llvm::Error initialize(const DepResultTs &...) override;
+///   llvm::Expected<bool> step() override;
+/// and may override finalize().
+///
+/// Dependencies are fixed for the lifetime of the analysis — initialize()
+/// binds them once, step() is called until it returns false, and
+/// finalize() post-processes after convergence.
+template <typename ResultT, typename... DepResultTs>
+class DerivedAnalysis : public DerivedAnalysisBase {
+  static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
+                "ResultT must derive from AnalysisResult");
+  static_assert(HasAnalysisName<ResultT>::value,
+                "ResultT must have a static analysisName() method");
+  static_assert((std::is_base_of_v<AnalysisResult, DepResultTs> && ...),
+                "Every DepResultT must derive from AnalysisResult");
+  static_assert((HasAnalysisName<DepResultTs>::value && ...),
+                "Every DepResultT must have a static analysisName() method");
+
+  std::unique_ptr<ResultT> Result;
+
+public:
+  DerivedAnalysis() : Result(std::make_unique<ResultT>()) {}
+
+  using ResultType = ResultT;
+
+  /// Used by AnalysisRegistry::Add to derive the registry entry name.
+  AnalysisName analysisName() const final { return ResultT::analysisName(); }
+
+  const std::vector<AnalysisName> &dependencyNames() const final {
+    static const std::vector<AnalysisName> Names = {
+        DepResultTs::analysisName()...};
+    return Names;
+  }
+
+  /// Called once with the fixed dependency results before the step() loop.
+  virtual llvm::Error initialize(const DepResultTs &...) = 0;
+
+  /// Performs one step. Returns true if another step is needed; false when
+  /// converged. Single-step analyses always return false.
+  virtual llvm::Expected<bool> step() = 0;
+
+  /// Called after the step() loop converges. Override for post-processing.
+  virtual llvm::Error finalize() { return llvm::Error::success(); }
+
+protected:
+  /// Read-only access to the result being built.
+  const ResultT &result() const & { return *Result; }
+
+  /// Mutable access to the result being built.
+  ResultT &result() & { return *Result; }
+
+private:
+  /// Seals the type-erased base overload, downcasts, and dispatches to the
+  /// typed initialize().
+  llvm::Error
+  initialize(const std::map<AnalysisName, const AnalysisResult *> &Map) final {
+    return initialize(*static_cast<const DepResultTs *>(
+        Map.at(DepResultTs::analysisName()))...);
+  }
+
+  /// Type-erased result extraction for the driver.
+  std::unique_ptr<AnalysisResult> result() && final {
+    return std::move(Result);
+  }
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
new file mode 100644
index 0000000000000..3a53ec147db4f
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -0,0 +1,132 @@
+//===- SummaryAnalysis.h --------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Defines SummaryAnalysisBase (type-erased base known to AnalysisDriver) and
+// the typed intermediate SummaryAnalysis<ResultT, EntitySummaryT> that
+// concrete analyses inherit from.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/EntitySummary.h"
+#include "llvm/Support/Error.h"
+#include <memory>
+
+namespace clang::ssaf {
+
+class AnalysisDriver;
+
+/// Type-erased base for summary analyses. Known to AnalysisDriver.
+///
+/// Not subclassed directly — use SummaryAnalysis<ResultT, EntitySummaryT>.
+/// A summary analysis processes per-entity EntitySummary objects from the
+/// LUSummary one at a time, accumulating whole-program data into an
+/// AnalysisResult.
+class SummaryAnalysisBase : public AnalysisBase {
+  friend class AnalysisDriver;
+
+protected:
+  SummaryAnalysisBase() : AnalysisBase(AnalysisBase::Kind::Summary) {}
+
+public:
+  /// SummaryName of the EntitySummary type this analysis consumes.
+  /// Used by the driver to route entities from the LUSummary.
+  virtual SummaryName summaryName() const = 0;
+
+private:
+  /// Called once before any add() calls. Default is a no-op.
+  virtual llvm::Error initialize() { return llvm::Error::success(); }
+
+  /// Called once per matching entity. The driver retains ownership of the
+  /// summary; multiple SummaryAnalysis instances may receive the same entity.
+  virtual llvm::Error add(EntityId Id, const EntitySummary &Summary) = 0;
+
+  /// Called after all entities have been processed. Default is a no-op.
+  virtual llvm::Error finalize() { return llvm::Error::success(); }
+
+  /// Transfers ownership of the built result. Called once after finalize().
+  /// The rvalue ref-qualifier enforces single use.
+  virtual std::unique_ptr<AnalysisResult> result() && = 0;
+};
+
+/// Typed intermediate that concrete summary analyses inherit from.
+///
+/// Concrete analyses must implement:
+///   llvm::Error add(EntityId Id, const EntitySummaryT &Summary) override;
+/// and may override initialize() and finalize().
+///
+/// The result being built is accessible via result() const & (read-only) and
+/// result() & (mutable) within the analysis implementation.
+template <typename ResultT, typename EntitySummaryT>
+class SummaryAnalysis : public SummaryAnalysisBase {
+  static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
+                "ResultT must derive from AnalysisResult");
+  static_assert(HasAnalysisName<ResultT>::value,
+                "ResultT must have a static analysisName() method");
+  static_assert(std::is_base_of_v<EntitySummary, EntitySummaryT>,
+                "EntitySummaryT must derive from EntitySummary");
+
+  std::unique_ptr<ResultT> Result;
+
+public:
+  SummaryAnalysis() : Result(std::make_unique<ResultT>()) {}
+
+  using ResultType = ResultT;
+
+  /// Used by AnalysisRegistry::Add to derive the registry entry name.
+  AnalysisName analysisName() const final { return ResultT::analysisName(); }
+
+  SummaryName summaryName() const final {
+    return EntitySummaryT::summaryName();
+  }
+
+  const std::vector<AnalysisName> &dependencyNames() const final {
+    static const std::vector<AnalysisName> Empty;
+    return Empty;
+  }
+
+  /// Called once before the first add() call. Override for initialization.
+  virtual llvm::Error initialize() override { return llvm::Error::success(); }
+
+  /// Called once per matching entity. Implement to accumulate data.
+  virtual llvm::Error add(EntityId Id, const EntitySummaryT &Summary) = 0;
+
+  /// Called after all entities have been processed. Override for
+  /// post-processing.
+  virtual llvm::Error finalize() override { return llvm::Error::success(); }
+
+protected:
+  /// Read-only access to the result being built.
+  const ResultT &result() const & { return *Result; }
+
+  /// Mutable access to the result being built.
+  ResultT &result() & { return *Result; }
+
+private:
+  /// Seals the type-erased base overload, downcasts, and dispatches to the
+  /// typed add().
+  llvm::Error add(EntityId Id, const EntitySummary &Summary) final {
+    return add(Id, static_cast<const EntitySummaryT &>(Summary));
+  }
+
+  /// Type-erased result extraction for the driver.
+  std::unique_ptr<AnalysisResult> result() && final {
+    return std::move(Result);
+  }
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
new file mode 100644
index 0000000000000..040f8ea79c0ec
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -0,0 +1,92 @@
+//===- WPASuite.h ---------------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// The value returned by AnalysisDriver::run(). Bundles the EntityIdTable
+// (moved from the LUSummary) with the analysis results keyed by AnalysisName.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityIdTable.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "llvm/Support/Error.h"
+#include <map>
+#include <memory>
+
+namespace clang::ssaf {
+
+class AnalysisDriver;
+
+/// Bundles the EntityIdTable (moved from the LUSummary) and the analysis
+/// results produced by one AnalysisDriver::run() call, keyed by AnalysisName.
+///
+/// This is the natural unit of persistence: entity names and analysis results
+/// are self-contained in one object.
+class WPASuite {
+  friend class AnalysisDriver;
+
+  EntityIdTable IdTable;
+  std::map<AnalysisName, std::unique_ptr<AnalysisResult>> Data;
+
+  WPASuite() = default;
+
+public:
+  /// Returns the EntityIdTable that maps EntityId values to their symbolic
+  /// names. Moved from the LUSummary during AnalysisDriver::run().
+  const EntityIdTable &idTable() const { return IdTable; }
+
+  /// Returns true if a result for \p ResultT is present.
+  template <typename ResultT> [[nodiscard]] bool contains() const {
+    static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
+                  "ResultT must derive from AnalysisResult");
+    static_assert(HasAnalysisName<ResultT>::value,
+                  "ResultT must have a static analysisName() method");
+
+    return contains(ResultT::analysisName());
+  }
+
+  /// Returns true if a result for \p Name is present.
+  [[nodiscard]] bool contains(AnalysisName Name) const {
+    return Data.find(Name) != Data.end();
+  }
+
+  /// Returns a reference to the result for \p ResultT, or an error if absent.
+  template <typename ResultT> [[nodiscard]] llvm::Expected<ResultT &> get() {
+    static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
+                  "ResultT must derive from AnalysisResult");
+    static_assert(HasAnalysisName<ResultT>::value,
+                  "ResultT must have a static analysisName() method");
+
+    auto Result = get(ResultT::analysisName());
+    if (!Result) {
+      return Result.takeError();
+    }
+    return static_cast<ResultT &>(*Result);
+  }
+
+  /// Returns a reference to the result for \p Name, or an error if absent.
+  [[nodiscard]] llvm::Expected<AnalysisResult &> get(AnalysisName Name) {
+    auto It = Data.find(Name);
+    if (It == Data.end()) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  "no result for analysis '{0}' in WPASuite",
+                                  Name.str())
+          .build();
+    }
+    return *It->second;
+  }
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
index 552fff04a4c01..44e7504009bee 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
@@ -34,6 +34,7 @@ class LUSummary {
   friend class LUSummaryConsumer;
   friend class SerializationFormat;
   friend class TestFixture;
+  friend class AnalysisDriver;
 
   NestedBuildNamespace LUNamespace;
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h
new file mode 100644
index 0000000000000..167d8a8b0485e
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h
@@ -0,0 +1,49 @@
+//===- AnalysisName.h -----------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Strong typedef identifying a whole-program analysis and its result type.
+// Distinct from SummaryName, which identifies per-entity EntitySummary types.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
+
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/raw_ostream.h"
+#include <string>
+
+namespace clang::ssaf {
+
+/// Uniquely identifies a whole-program analysis and the AnalysisResult it
+/// produces. Used as the key in WPASuite and AnalysisRegistry.
+///
+/// Distinct from SummaryName, which is used by EntitySummary types for routing
+/// through the LUSummary.
+class AnalysisName {
+public:
+  explicit AnalysisName(std::string Name) : Name(std::move(Name)) {}
+
+  bool operator==(const AnalysisName &Other) const {
+    return Name == Other.Name;
+  }
+  bool operator!=(const AnalysisName &Other) const { return !(*this == Other); }
+  bool operator<(const AnalysisName &Other) const { return Name < Other.Name; }
+
+  /// Explicit conversion to the underlying string representation.
+  llvm::StringRef str() const { return Name; }
+
+private:
+  std::string Name;
+};
+
+llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const AnalysisName &AN);
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h
new file mode 100644
index 0000000000000..888f4a8e6be4a
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h
@@ -0,0 +1,33 @@
+//===- AnalysisTraits.h ---------------------------------------------------===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// Type traits for AnalysisResult subclasses.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include <type_traits>
+
+namespace clang::ssaf {
+
+/// Type trait that checks whether \p T has a static \c analysisName() method
+/// returning \c AnalysisName. Used to enforce the convention on AnalysisResult
+/// subclasses and analysis classes at instantiation time.
+template <typename T, typename = void>
+struct HasAnalysisName : std::false_type {};
+
+template <typename T>
+struct HasAnalysisName<T, std::void_t<decltype(T::analysisName())>>
+    : std::is_same<decltype(T::analysisName()), AnalysisName> {};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
index d49fd6cb4a1dc..f50d17d7f035a 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
@@ -14,6 +14,7 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
@@ -24,6 +25,13 @@
 
 namespace llvm {
 
+template <> struct format_provider<clang::ssaf::AnalysisName> {
+  static void format(const clang::ssaf::AnalysisName &Val, raw_ostream &OS,
+                     StringRef Style) {
+    OS << Val;
+  }
+};
+
 template <> struct format_provider<clang::ssaf::EntityId> {
   static void format(const clang::ssaf::EntityId &Val, raw_ostream &OS,
                      StringRef Style) {
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
new file mode 100644
index 0000000000000..64b496142cd47
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -0,0 +1,196 @@
+//===- AnalysisDriver.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/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
+#include <map>
+#include <vector>
+
+using namespace clang;
+using namespace ssaf;
+
+AnalysisDriver::AnalysisDriver(std::unique_ptr<LUSummary> LU)
+    : LU(std::move(LU)) {}
+
+llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
+AnalysisDriver::sortTopologically(llvm::ArrayRef<AnalysisName> Roots) {
+  struct Visitor {
+    enum class State { Unvisited, Visiting, Visited };
+
+    std::map<AnalysisName, State> Marks;
+    std::map<AnalysisName, std::unique_ptr<AnalysisBase>> Analyses;
+    std::vector<std::unique_ptr<AnalysisBase>> Result;
+
+    State getState(const AnalysisName &Name) {
+      auto MarkIt = Marks.find(Name);
+      return MarkIt != Marks.end() ? MarkIt->second : State::Unvisited;
+    }
+
+    void setState(const AnalysisName &Name, State S) { Marks[Name] = S; }
+
+    llvm::Error visit(const AnalysisName &Name) {
+      State S = getState(Name);
+
+      if (S == State::Visited) {
+        return llvm::Error::success();
+      }
+
+      if (S == State::Visiting) {
+        return ErrorBuilder::create(std::errc::invalid_argument,
+                                    "cycle detected involving analysis '{0}'",
+                                    Name)
+            .build();
+      }
+
+      if (S == State::Unvisited) {
+        setState(Name, State::Visiting);
+
+        auto V = AnalysisRegistry::instantiate(Name.str());
+        if (!V) {
+          return ErrorBuilder::create(std::errc::invalid_argument,
+                                      "no analysis registered for '{0}'", Name)
+              .build();
+        }
+
+        const auto &Deps = (*V)->dependencyNames();
+        Analyses[Name] = std::move(*V);
+
+        for (const auto &Dep : Deps) {
+          if (auto Err = visit(Dep)) {
+            return Err;
+          }
+        }
+
+        setState(Name, State::Visited);
+        Result.push_back(std::move(Analyses[Name]));
+        Analyses.erase(Name);
+        return llvm::Error::success();
+      }
+      llvm_unreachable("unhandled State");
+    }
+  };
+
+  Visitor V;
+  for (const auto &Root : Roots) {
+    if (auto Err = V.visit(Root)) {
+      return std::move(Err);
+    }
+  }
+  return std::move(V.Result);
+}
+
+llvm::Error AnalysisDriver::executeSummaryAnalysis(
+    std::unique_ptr<SummaryAnalysisBase> Summary, WPASuite &Suite) {
+  SummaryName SN = Summary->summaryName();
+  auto DataIt = LU->Data.find(SN);
+  if (DataIt == LU->Data.end()) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                "no data for analysis '{0}' in LUSummary",
+                                Summary->analysisName().str())
+        .build();
+  }
+
+  if (auto Err = Summary->initialize()) {
+    return Err;
+  }
+
+  for (auto &[Id, EntitySummary] : DataIt->second) {
+    if (auto Err = Summary->add(Id, *EntitySummary)) {
+      return Err;
+    }
+  }
+
+  if (auto Err = Summary->finalize()) {
+    return Err;
+  }
+
+  Suite.Data.emplace(Summary->analysisName(), std::move(*Summary).result());
+
+  return llvm::Error::success();
+}
+
+llvm::Error AnalysisDriver::executeDerivedAnalysis(
+    std::unique_ptr<DerivedAnalysisBase> Derived, WPASuite &Suite) {
+  std::map<AnalysisName, const AnalysisResult *> DepMap;
+
+  for (const auto &DepName : Derived->dependencyNames()) {
+    auto It = Suite.Data.find(DepName);
+    if (It == Suite.Data.end()) {
+      ErrorBuilder::fatal("missing dependency '{0}' for analysis '{1}': "
+                          "dependency graph is not topologically sorted",
+                          DepName.str(), Derived->analysisName().str());
+    }
+    DepMap[DepName] = It->second.get();
+  }
+
+  if (auto Err = Derived->initialize(DepMap)) {
+    return Err;
+  }
+
+  while (true) {
+    auto StepOrErr = Derived->step();
+    if (!StepOrErr) {
+      return StepOrErr.takeError();
+    }
+    if (!*StepOrErr) {
+      break;
+    }
+  }
+
+  if (auto Err = Derived->finalize()) {
+    return Err;
+  }
+
+  Suite.Data.emplace(Derived->analysisName(), std::move(*Derived).result());
+
+  return llvm::Error::success();
+}
+
+llvm::Expected<WPASuite>
+AnalysisDriver::execute(EntityIdTable IdTable,
+                        std::vector<std::unique_ptr<AnalysisBase>> Sorted) {
+  WPASuite Suite;
+  Suite.IdTable = std::move(IdTable);
+
+  for (auto &V : Sorted) {
+    if (V->TheKind == AnalysisBase::Kind::Summary) {
+      auto SA = std::unique_ptr<SummaryAnalysisBase>(
+          static_cast<SummaryAnalysisBase *>(V.release()));
+      if (auto Err = executeSummaryAnalysis(std::move(SA), Suite)) {
+        return std::move(Err);
+      }
+    } else {
+      auto DA = std::unique_ptr<DerivedAnalysisBase>(
+          static_cast<DerivedAnalysisBase *>(V.release()));
+      if (auto Err = executeDerivedAnalysis(std::move(DA), Suite)) {
+        return std::move(Err);
+      }
+    }
+  }
+
+  return Suite;
+}
+
+llvm::Expected<WPASuite> AnalysisDriver::run() && {
+  return run(AnalysisRegistry::names());
+}
+
+llvm::Expected<WPASuite>
+AnalysisDriver::run(llvm::ArrayRef<AnalysisName> Names) {
+  auto ExpectedSorted = sortTopologically(Names);
+  if (!ExpectedSorted) {
+    return ExpectedSorted.takeError();
+  }
+
+  return execute(LU->IdTable, std::move(*ExpectedSorted));
+}
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
new file mode 100644
index 0000000000000..aac05fdb08453
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
@@ -0,0 +1,37 @@
+//===- AnalysisRegistry.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/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "llvm/ADT/STLExtras.h"
+
+using namespace clang;
+using namespace ssaf;
+
+using RegistryT = llvm::Registry<AnalysisBase>;
+
+LLVM_INSTANTIATE_REGISTRY(RegistryT)
+
+std::vector<AnalysisName> AnalysisRegistry::analysisNames;
+
+bool AnalysisRegistry::contains(llvm::StringRef Name) {
+  return llvm::is_contained(analysisNames, AnalysisName(std::string(Name)));
+}
+
+const std::vector<AnalysisName> &AnalysisRegistry::names() {
+  return analysisNames;
+}
+
+std::optional<std::unique_ptr<AnalysisBase>>
+AnalysisRegistry::instantiate(llvm::StringRef Name) {
+  for (const auto &Entry : RegistryT::entries()) {
+    if (Entry.getName() == Name) {
+      return std::unique_ptr<AnalysisBase>(Entry.instantiate());
+    }
+  }
+  return std::nullopt;
+}
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
index 190b9fe400a64..9e9786dae5a07 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
@@ -4,7 +4,10 @@ set(LLVM_LINK_COMPONENTS
 
 add_clang_library(clangScalableStaticAnalysisFrameworkCore
   ASTEntityMapping.cpp
+  Analysis/AnalysisDriver.cpp
+  Analysis/AnalysisRegistry.cpp
   EntityLinker/EntityLinker.cpp
+  Model/AnalysisName.cpp
   Model/BuildNamespace.cpp
   Model/EntityId.cpp
   Model/EntityIdTable.cpp
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp
new file mode 100644
index 0000000000000..95e0c02f9a92f
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp
@@ -0,0 +1,16 @@
+//===- AnalysisName.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/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+
+using namespace clang::ssaf;
+
+llvm::raw_ostream &clang::ssaf::operator<<(llvm::raw_ostream &OS,
+                                           const AnalysisName &AN) {
+  return OS << "AnalysisName(" << AN.str() << ")";
+}
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
new file mode 100644
index 0000000000000..436e0da580910
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -0,0 +1,390 @@
+//===- AnalysisDriverTest.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/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h"
+#include "../TestFixture.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+#include <memory>
+#include <utility>
+#include <vector>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+// ---------------------------------------------------------------------------
+// Instance counter
+// ---------------------------------------------------------------------------
+
+static int NextSummaryInstanceId = 0;
+
+// ---------------------------------------------------------------------------
+// Entity summaries
+// ---------------------------------------------------------------------------
+
+class Analysis1EntitySummary final : public EntitySummary {
+public:
+  int InstanceId = NextSummaryInstanceId++;
+  static SummaryName summaryName() { return SummaryName("Analysis1"); }
+  SummaryName getSummaryName() const override {
+    return SummaryName("Analysis1");
+  }
+};
+
+class Analysis2EntitySummary final : public EntitySummary {
+public:
+  int InstanceId = NextSummaryInstanceId++;
+  static SummaryName summaryName() { return SummaryName("Analysis2"); }
+  SummaryName getSummaryName() const override {
+    return SummaryName("Analysis2");
+  }
+};
+
+class Analysis3EntitySummary final : public EntitySummary {
+public:
+  int InstanceId = NextSummaryInstanceId++;
+  static SummaryName summaryName() { return SummaryName("Analysis3"); }
+  SummaryName getSummaryName() const override {
+    return SummaryName("Analysis3");
+  }
+};
+
+class Analysis4EntitySummary final : public EntitySummary {
+public:
+  int InstanceId = NextSummaryInstanceId++;
+  static SummaryName summaryName() { return SummaryName("Analysis4"); }
+  SummaryName getSummaryName() const override {
+    return SummaryName("Analysis4");
+  }
+};
+
+// ---------------------------------------------------------------------------
+// Results
+// ---------------------------------------------------------------------------
+
+class Analysis1Result final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("Analysis1"); }
+  std::vector<std::pair<EntityId, int>> Entries;
+  bool WasFinalized = false;
+};
+
+class Analysis2Result final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("Analysis2"); }
+  std::vector<std::pair<EntityId, int>> Entries;
+  bool WasFinalized = false;
+};
+
+// No analysis or registration for Analysis3. Data for Analysis3 is inserted
+// into the LUSummary to verify the driver silently skips it.
+class Analysis3Result final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("Analysis3"); }
+};
+
+// Analysis4 has a registered analysis but no data is inserted into the
+// LUSummary, so it is skipped and get() returns nullptr.
+class Analysis4Result final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("Analysis4"); }
+  std::vector<std::pair<EntityId, int>> Entries;
+  bool WasFinalized = false;
+};
+
+// ---------------------------------------------------------------------------
+// Analysis destruction flags (reset in SetUp)
+// ---------------------------------------------------------------------------
+
+static bool Analysis1WasDestroyed = false;
+static bool Analysis2WasDestroyed = false;
+static bool Analysis4WasDestroyed = false;
+
+// ---------------------------------------------------------------------------
+// Analyses
+// ---------------------------------------------------------------------------
+
+class Analysis1 final
+    : public SummaryAnalysis<Analysis1Result, Analysis1EntitySummary> {
+public:
+  ~Analysis1() { Analysis1WasDestroyed = true; }
+
+  llvm::Error add(EntityId Id, const Analysis1EntitySummary &S) override {
+    result().Entries.push_back({Id, S.InstanceId});
+    return llvm::Error::success();
+  }
+
+  llvm::Error finalize() override {
+    result().WasFinalized = true;
+    return llvm::Error::success();
+  }
+};
+
+static AnalysisRegistry::Add<Analysis1> RegAnalysis1("Analysis for Analysis1");
+
+class Analysis2 final
+    : public SummaryAnalysis<Analysis2Result, Analysis2EntitySummary> {
+public:
+  ~Analysis2() { Analysis2WasDestroyed = true; }
+
+  llvm::Error add(EntityId Id, const Analysis2EntitySummary &S) override {
+    result().Entries.push_back({Id, S.InstanceId});
+    return llvm::Error::success();
+  }
+
+  llvm::Error finalize() override {
+    result().WasFinalized = true;
+    return llvm::Error::success();
+  }
+};
+
+static AnalysisRegistry::Add<Analysis2> RegAnalysis2("Analysis for Analysis2");
+
+class Analysis4 final
+    : public SummaryAnalysis<Analysis4Result, Analysis4EntitySummary> {
+public:
+  ~Analysis4() { Analysis4WasDestroyed = true; }
+
+  llvm::Error add(EntityId Id, const Analysis4EntitySummary &S) override {
+    result().Entries.push_back({Id, S.InstanceId});
+    return llvm::Error::success();
+  }
+
+  llvm::Error finalize() override {
+    result().WasFinalized = true;
+    return llvm::Error::success();
+  }
+};
+
+static AnalysisRegistry::Add<Analysis4> RegAnalysis4("Analysis for Analysis4");
+
+// ---------------------------------------------------------------------------
+// Fixture
+// ---------------------------------------------------------------------------
+
+class AnalysisDriverTest : public TestFixture {
+protected:
+  static constexpr EntityLinkage ExternalLinkage =
+      EntityLinkage(EntityLinkageType::External);
+
+  void SetUp() override {
+    NextSummaryInstanceId = 0;
+    Analysis1WasDestroyed = false;
+    Analysis2WasDestroyed = false;
+    Analysis4WasDestroyed = false;
+  }
+
+  std::unique_ptr<LUSummary> makeLUSummary() {
+    NestedBuildNamespace NS(
+        {BuildNamespace(BuildNamespaceKind::LinkUnit, "TestLU")});
+    return std::make_unique<LUSummary>(std::move(NS));
+  }
+
+  EntityId addEntity(LUSummary &LU, llvm::StringRef USR) {
+    NestedBuildNamespace NS(
+        {BuildNamespace(BuildNamespaceKind::LinkUnit, "TestLU")});
+    EntityName Name(USR.str(), "", NS);
+    EntityId Id = getIdTable(LU).getId(Name);
+    getLinkageTable(LU).insert({Id, ExternalLinkage});
+    return Id;
+  }
+
+  static bool hasEntry(const std::vector<std::pair<EntityId, int>> &Entries,
+                       EntityId Id, int InstanceId) {
+    return llvm::is_contained(Entries, std::make_pair(Id, InstanceId));
+  }
+
+  template <typename SummaryT>
+  int insertSummary(LUSummary &LU, llvm::StringRef SN, EntityId Id) {
+    auto S = std::make_unique<SummaryT>();
+    int InstanceId = S->InstanceId;
+    getData(LU)[SummaryName(SN.str())][Id] = std::move(S);
+    return InstanceId;
+  }
+};
+
+// ---------------------------------------------------------------------------
+// Tests
+// ---------------------------------------------------------------------------
+
+TEST(AnalysisRegistryTest, AnalysisIsRegistered) {
+  EXPECT_FALSE(AnalysisRegistry::contains("AnalysisNonExisting"));
+  EXPECT_TRUE(AnalysisRegistry::contains("Analysis1"));
+  EXPECT_TRUE(AnalysisRegistry::contains("Analysis2"));
+  EXPECT_TRUE(AnalysisRegistry::contains("Analysis4"));
+}
+
+TEST(AnalysisRegistryTest, AnalysisCanBeInstantiated) {
+  EXPECT_FALSE(
+      AnalysisRegistry::instantiate("AnalysisNonExisting").has_value());
+  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis1").has_value());
+  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis2").has_value());
+  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis4").has_value());
+}
+
+// run() — processes all registered analyses present in the LUSummary.
+// Silently skips data whose analysis is unregistered (Analysis3).
+TEST_F(AnalysisDriverTest, RunAll) {
+  auto LU = makeLUSummary();
+  const auto E1 = addEntity(*LU, "Entity1");
+  const auto E2 = addEntity(*LU, "Entity2");
+  const auto E3 = addEntity(*LU, "Entity3");
+  const auto E4 = addEntity(*LU, "Entity4");
+
+  int s1a = insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E1);
+  int s1b = insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E2);
+  int s2a = insertSummary<Analysis2EntitySummary>(*LU, "Analysis2", E2);
+  int s2b = insertSummary<Analysis2EntitySummary>(*LU, "Analysis2", E3);
+  int s4a = insertSummary<Analysis4EntitySummary>(*LU, "Analysis4", E4);
+
+  // No registered analysis — Analysis3 data silently skipped.
+  (void)insertSummary<Analysis3EntitySummary>(*LU, "Analysis3", E1);
+
+  AnalysisDriver Driver(std::move(LU));
+  auto WPAOrErr = std::move(Driver).run();
+  ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
+
+  {
+    auto R1OrErr = WPAOrErr->get<Analysis1Result>();
+    ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded());
+    EXPECT_EQ(R1OrErr->Entries.size(), 2u);
+    EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
+    EXPECT_TRUE(hasEntry(R1OrErr->Entries, E2, s1b));
+    EXPECT_TRUE(R1OrErr->WasFinalized);
+    EXPECT_TRUE(Analysis1WasDestroyed);
+  }
+
+  {
+    auto R2OrErr = WPAOrErr->get<Analysis2Result>();
+    ASSERT_THAT_EXPECTED(R2OrErr, llvm::Succeeded());
+    EXPECT_EQ(R2OrErr->Entries.size(), 2u);
+    EXPECT_TRUE(hasEntry(R2OrErr->Entries, E2, s2a));
+    EXPECT_TRUE(hasEntry(R2OrErr->Entries, E3, s2b));
+    EXPECT_TRUE(R2OrErr->WasFinalized);
+    EXPECT_TRUE(Analysis2WasDestroyed);
+  }
+
+  {
+    auto R4OrErr = WPAOrErr->get<Analysis4Result>();
+    ASSERT_THAT_EXPECTED(R4OrErr, llvm::Succeeded());
+    EXPECT_EQ(R4OrErr->Entries.size(), 1u);
+    EXPECT_TRUE(hasEntry(R4OrErr->Entries, E4, s4a));
+    EXPECT_TRUE(R4OrErr->WasFinalized);
+    EXPECT_TRUE(Analysis4WasDestroyed);
+  }
+
+  // Unregistered analysis — not present in WPA.
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis3Result>(), llvm::Failed());
+}
+
+// run(names) — processes only the analyses for the given names.
+TEST_F(AnalysisDriverTest, RunByName) {
+  auto LU = makeLUSummary();
+  const auto E1 = addEntity(*LU, "Entity1");
+  const auto E2 = addEntity(*LU, "Entity2");
+
+  int s1a = insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E1);
+  insertSummary<Analysis2EntitySummary>(*LU, "Analysis2", E2);
+
+  AnalysisDriver Driver(std::move(LU));
+  auto WPAOrErr = Driver.run({AnalysisName("Analysis1")});
+  ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
+
+  // Analysis1 was requested and has data — present.
+  auto R1OrErr = WPAOrErr->get<Analysis1Result>();
+  ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded());
+  EXPECT_EQ(R1OrErr->Entries.size(), 1u);
+  EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
+  EXPECT_TRUE(R1OrErr->WasFinalized);
+
+  // Analysis2 was not requested — not present even though data exists.
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(), llvm::Failed());
+}
+
+// run(names) — error when a requested name has no data in LUSummary.
+TEST_F(AnalysisDriverTest, RunByNameErrorMissingData) {
+  auto LU = makeLUSummary();
+  AnalysisDriver Driver(std::move(LU));
+
+  EXPECT_THAT_EXPECTED(Driver.run({AnalysisName("Analysis1")}), llvm::Failed());
+}
+
+// run(names) — error when a requested name has no registered analysis.
+TEST_F(AnalysisDriverTest, RunByNameErrorMissingAnalysis) {
+  auto LU = makeLUSummary();
+  const auto E1 = addEntity(*LU, "Entity1");
+  insertSummary<Analysis3EntitySummary>(*LU, "Analysis3", E1);
+
+  AnalysisDriver Driver(std::move(LU));
+
+  // Analysis3 has data but no registered analysis.
+  EXPECT_THAT_EXPECTED(Driver.run({AnalysisName("Analysis3")}), llvm::Failed());
+}
+
+// run<ResultTs...>() — type-safe subset.
+TEST_F(AnalysisDriverTest, RunByType) {
+  auto LU = makeLUSummary();
+  const auto E1 = addEntity(*LU, "Entity1");
+  const auto E2 = addEntity(*LU, "Entity2");
+
+  int s1a = insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E1);
+  insertSummary<Analysis2EntitySummary>(*LU, "Analysis2", E2);
+
+  AnalysisDriver Driver(std::move(LU));
+  auto WPAOrErr = Driver.run<Analysis1Result>();
+  ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
+
+  // Analysis1 was requested — present.
+  auto R1OrErr = WPAOrErr->get<Analysis1Result>();
+  ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded());
+  EXPECT_EQ(R1OrErr->Entries.size(), 1u);
+  EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
+  EXPECT_TRUE(R1OrErr->WasFinalized);
+
+  // Analysis2 was not requested — not present even though data exists.
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(), llvm::Failed());
+}
+
+// run<ResultTs...>() — error when a requested type has no data in LUSummary.
+TEST_F(AnalysisDriverTest, RunByTypeErrorMissingData) {
+  auto LU = makeLUSummary();
+  AnalysisDriver Driver(std::move(LU));
+
+  EXPECT_THAT_EXPECTED(Driver.run<Analysis1Result>(), llvm::Failed());
+}
+
+// contains() — present entries return true; absent entries return false.
+TEST_F(AnalysisDriverTest, Contains) {
+  auto LU = makeLUSummary();
+  const auto E1 = addEntity(*LU, "Entity1");
+  insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E1);
+  insertSummary<Analysis4EntitySummary>(*LU, "Analysis4", E1);
+
+  AnalysisDriver Driver(std::move(LU));
+  auto WPAOrErr = std::move(Driver).run();
+  ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
+
+  EXPECT_TRUE(WPAOrErr->contains<Analysis1Result>());
+  EXPECT_FALSE(WPAOrErr->contains<Analysis2Result>());
+}
+
+} // namespace
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 7652ebb390f86..2fec611718475 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -1,5 +1,6 @@
 add_distinct_clang_unittest(ClangScalableAnalysisTests
   Analyses/UnsafeBufferUsage/UnsafeBufferUsageTest.cpp
+  Analysis/AnalysisDriverTest.cpp
   ASTEntityMappingTest.cpp
   BuildNamespaceTest.cpp
   EntityIdTableTest.cpp

>From 6478cc09e7ff9fe17eeabd45fa2edb4ed09f89a6 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 08:04:19 -0700
Subject: [PATCH 02/30] Move

---
 .../Core/Analysis/AnalysisBase.h                          | 2 +-
 .../Core/{Model => Analysis}/AnalysisName.h               | 6 +++---
 .../Core/Analysis/AnalysisRegistry.h                      | 2 +-
 .../Core/{Model => Analysis}/AnalysisTraits.h             | 8 ++++----
 .../Core/Analysis/DerivedAnalysis.h                       | 4 ++--
 .../Core/Analysis/SummaryAnalysis.h                       | 2 +-
 .../Core/Analysis/WPASuite.h                              | 4 ++--
 .../Core/Support/FormatProviders.h                        | 2 +-
 .../Core/{Model => Analysis}/AnalysisName.cpp             | 2 +-
 .../ScalableStaticAnalysisFramework/Core/CMakeLists.txt   | 2 +-
 .../Analysis/AnalysisDriverTest.cpp                       | 2 +-
 11 files changed, 18 insertions(+), 18 deletions(-)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Model => Analysis}/AnalysisName.h (86%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Model => Analysis}/AnalysisTraits.h (76%)
 rename clang/lib/ScalableStaticAnalysisFramework/Core/{Model => Analysis}/AnalysisName.cpp (88%)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
index 478a6fa85d4a8..873d21150ce87 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
@@ -15,7 +15,7 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include <vector>
 
 namespace clang::ssaf {
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
similarity index 86%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
index 167d8a8b0485e..73ba96ccc594e 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
@@ -11,8 +11,8 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
@@ -46,4 +46,4 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const AnalysisName &AN);
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISNAME_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
index e4faba62c070e..f51d7845b36e9 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -22,9 +22,9 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
 
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "llvm/Support/Registry.h"
 #include <memory>
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
similarity index 76%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
index 888f4a8e6be4a..ef6a5a56d990a 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
@@ -10,10 +10,10 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include <type_traits>
 
 namespace clang::ssaf {
@@ -30,4 +30,4 @@ struct HasAnalysisName<T, std::void_t<decltype(T::analysisName())>>
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_MODEL_ANALYSISTRAITS_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index 7b5695dab7c72..a4de8b186fc76 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -16,9 +16,9 @@
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
 
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "llvm/Support/Error.h"
 #include <map>
 #include <memory>
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
index 3a53ec147db4f..2c0f6e7821771 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -17,7 +17,7 @@
 
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/EntitySummary.h"
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index 040f8ea79c0ec..c1a48bbdd3e2b 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -14,9 +14,9 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
 
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityIdTable.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "llvm/Support/Error.h"
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
index f50d17d7f035a..1f8e34868c1d7 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
@@ -14,7 +14,7 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp
similarity index 88%
rename from clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp
rename to clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp
index 95e0c02f9a92f..d49f41ab24eb8 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp
@@ -6,7 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 
 using namespace clang::ssaf;
 
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
index 9e9786dae5a07..7951e77e7c10b 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
@@ -5,9 +5,9 @@ set(LLVM_LINK_COMPONENTS
 add_clang_library(clangScalableStaticAnalysisFrameworkCore
   ASTEntityMapping.cpp
   Analysis/AnalysisDriver.cpp
+  Analysis/AnalysisName.cpp
   Analysis/AnalysisRegistry.cpp
   EntityLinker/EntityLinker.cpp
-  Model/AnalysisName.cpp
   Model/BuildNamespace.cpp
   Model/EntityId.cpp
   Model/EntityIdTable.cpp
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
index 436e0da580910..22554764adf80 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -8,12 +8,12 @@
 
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h"
 #include "../TestFixture.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Model/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"

>From 949626d8550924d8987ef836fcf507da2fd9e169 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 08:12:38 -0700
Subject: [PATCH 03/30] Fixes

---
 .../Core/Analysis/AnalysisBase.h              |  2 +-
 .../Core/Analysis/AnalysisDriver.h            | 16 +++++-----
 .../Core/Analysis/AnalysisResult.h            |  2 +-
 .../Core/Analysis/DerivedAnalysis.h           | 16 +++++++---
 .../Core/Analysis/SummaryAnalysis.h           |  2 +-
 .../Core/Analysis/WPASuite.h                  | 30 +++++++++++++++++++
 .../Core/Analysis/AnalysisDriver.cpp          |  6 ++--
 7 files changed, 57 insertions(+), 17 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
index 873d21150ce87..29c46f3c2e544 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
@@ -26,7 +26,7 @@ class DerivedAnalysisBase;
 
 /// Minimal common base for both analysis kinds.
 ///
-/// Not subclassed directly — use SummaryAnalysis<...> or
+/// Not subclassed directly -- use SummaryAnalysis<...> or
 /// DerivedAnalysis<...> instead.
 class AnalysisBase {
   friend class AnalysisDriver;
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
index 1cc5c18d348b9..3ac0d64e7de46 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
@@ -28,14 +28,14 @@ namespace clang::ssaf {
 /// Orchestrates whole-program analysis over an LUSummary.
 ///
 /// Three run() patterns are supported:
-///   - run() &&        — all registered analyses; silently skips any whose
-///                       entity data is absent or whose dependency was skipped.
-///                       Requires an rvalue driver because this exhausts the
-///                       LUSummary.
-///   - run(names)      — named subset plus transitive dependencies; returns
-///                       Expected and fails if any listed name has no
-///                       registered analysis or missing entity data.
-///   - run<ResultTs..> — type-safe variant of run(names).
+///   - run() &&        -- all registered analyses; silently skips any whose
+///                        entity data is absent or whose dependency was
+///                        skipped. Requires an rvalue driver because this
+///                        exhausts the LUSummary.
+///   - run(names)      -- named subset plus transitive dependencies; returns
+///                        Expected and fails if any listed name has no
+///                        registered analysis or missing entity data.
+///   - run<ResultTs..> -- type-safe variant of run(names).
 class AnalysisDriver final {
 public:
   explicit AnalysisDriver(std::unique_ptr<LUSummary> LU);
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
index 87d781cff30a8..7ac2a9ad7db6a 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
@@ -7,7 +7,7 @@
 //===----------------------------------------------------------------------===//
 //
 // Base class for all whole-program analysis results produced by AnalysisDriver.
-// Replaces SummaryData. Concrete subclasses carry a static analysisName().
+// Concrete subclasses carry a static analysisName().
 //
 //===----------------------------------------------------------------------===//
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index a4de8b186fc76..ecbf31c70c5fe 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -20,6 +20,7 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "llvm/Support/Error.h"
+#include "llvm/Support/ErrorHandling.h"
 #include <map>
 #include <memory>
 #include <vector>
@@ -30,7 +31,7 @@ class AnalysisDriver;
 
 /// Type-erased base for derived analyses. Known to AnalysisDriver.
 ///
-/// Not subclassed directly — use DerivedAnalysis<ResultT, DepResultTs...>.
+/// Not subclassed directly -- use DerivedAnalysis<ResultT, DepResultTs...>.
 /// A derived analysis consumes previously produced AnalysisResult objects
 /// and computes a new one via an initialize/step/finalize lifecycle.
 class DerivedAnalysisBase : public AnalysisBase {
@@ -66,7 +67,7 @@ class DerivedAnalysisBase : public AnalysisBase {
 ///   llvm::Expected<bool> step() override;
 /// and may override finalize().
 ///
-/// Dependencies are fixed for the lifetime of the analysis — initialize()
+/// Dependencies are fixed for the lifetime of the analysis: initialize()
 /// binds them once, step() is called until it returns false, and
 /// finalize() post-processes after convergence.
 template <typename ResultT, typename... DepResultTs>
@@ -115,11 +116,18 @@ class DerivedAnalysis : public DerivedAnalysisBase {
 
 private:
   /// Seals the type-erased base overload, downcasts, and dispatches to the
-  /// typed initialize().
+  /// typed initialize(). All dependencies are guaranteed present by the driver.
   llvm::Error
   initialize(const std::map<AnalysisName, const AnalysisResult *> &Map) final {
+    auto lookup = [&Map](const AnalysisName &Name) -> const AnalysisResult * {
+      auto It = Map.find(Name);
+      if (It == Map.end())
+        llvm_unreachable("dependency missing from DepResults map; "
+                         "dependency graph is not topologically sorted");
+      return It->second;
+    };
     return initialize(*static_cast<const DepResultTs *>(
-        Map.at(DepResultTs::analysisName()))...);
+        lookup(DepResultTs::analysisName()))...);
   }
 
   /// Type-erased result extraction for the driver.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
index 2c0f6e7821771..138e0e4754b5e 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -30,7 +30,7 @@ class AnalysisDriver;
 
 /// Type-erased base for summary analyses. Known to AnalysisDriver.
 ///
-/// Not subclassed directly — use SummaryAnalysis<ResultT, EntitySummaryT>.
+/// Not subclassed directly -- use SummaryAnalysis<ResultT, EntitySummaryT>.
 /// A summary analysis processes per-entity EntitySummary objects from the
 /// LUSummary one at a time, accumulating whole-program data into an
 /// AnalysisResult.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index c1a48bbdd3e2b..33a7124fbacf8 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -74,6 +74,22 @@ class WPASuite {
     return static_cast<ResultT &>(*Result);
   }
 
+  /// Returns a const reference to the result for \p ResultT, or an error if
+  /// absent.
+  template <typename ResultT>
+  [[nodiscard]] llvm::Expected<const ResultT &> get() const {
+    static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
+                  "ResultT must derive from AnalysisResult");
+    static_assert(HasAnalysisName<ResultT>::value,
+                  "ResultT must have a static analysisName() method");
+
+    auto Result = get(ResultT::analysisName());
+    if (!Result) {
+      return Result.takeError();
+    }
+    return static_cast<const ResultT &>(*Result);
+  }
+
   /// Returns a reference to the result for \p Name, or an error if absent.
   [[nodiscard]] llvm::Expected<AnalysisResult &> get(AnalysisName Name) {
     auto It = Data.find(Name);
@@ -85,6 +101,20 @@ class WPASuite {
     }
     return *It->second;
   }
+
+  /// Returns a const reference to the result for \p Name, or an error if
+  /// absent.
+  [[nodiscard]] llvm::Expected<const AnalysisResult &>
+  get(AnalysisName Name) const {
+    auto It = Data.find(Name);
+    if (It == Data.end()) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  "no result for analysis '{0}' in WPASuite",
+                                  Name.str())
+          .build();
+    }
+    return *It->second;
+  }
 };
 
 } // namespace clang::ssaf
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index 64b496142cd47..41fd55ce88c6b 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -114,7 +114,8 @@ llvm::Error AnalysisDriver::executeSummaryAnalysis(
     return Err;
   }
 
-  Suite.Data.emplace(Summary->analysisName(), std::move(*Summary).result());
+  AnalysisName Name = Summary->analysisName();
+  Suite.Data.emplace(Name, std::move(*Summary).result());
 
   return llvm::Error::success();
 }
@@ -151,7 +152,8 @@ llvm::Error AnalysisDriver::executeDerivedAnalysis(
     return Err;
   }
 
-  Suite.Data.emplace(Derived->analysisName(), std::move(*Derived).result());
+  AnalysisName Name = Derived->analysisName();
+  Suite.Data.emplace(Name, std::move(*Derived).result());
 
   return llvm::Error::success();
 }

>From 53f52415c6c7a848b689731558a530a0b673a449 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 08:14:26 -0700
Subject: [PATCH 04/30] More Fix

---
 .../Core/Support/FormatProviders.h                 | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
index 1f8e34868c1d7..6cc816edfd967 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
@@ -25,13 +25,6 @@
 
 namespace llvm {
 
-template <> struct format_provider<clang::ssaf::AnalysisName> {
-  static void format(const clang::ssaf::AnalysisName &Val, raw_ostream &OS,
-                     StringRef Style) {
-    OS << Val;
-  }
-};
-
 template <> struct format_provider<clang::ssaf::EntityId> {
   static void format(const clang::ssaf::EntityId &Val, raw_ostream &OS,
                      StringRef Style) {
@@ -88,6 +81,13 @@ template <> struct format_provider<clang::ssaf::SummaryName> {
   }
 };
 
+template <> struct format_provider<clang::ssaf::AnalysisName> {
+  static void format(const clang::ssaf::AnalysisName &Val, raw_ostream &OS,
+                     StringRef Style) {
+    OS << Val;
+  }
+};
+
 } // namespace llvm
 
 #endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H

>From 5963996bb6734384145a702d5415430e9a761516 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 08:36:36 -0700
Subject: [PATCH 05/30] Remove non-const methods

---
 .../Core/Analysis/DerivedAnalysis.h           |  3 ++-
 .../Core/Analysis/WPASuite.h                  | 26 -------------------
 2 files changed, 2 insertions(+), 27 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index ecbf31c70c5fe..e8c2f2fb8c188 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -121,9 +121,10 @@ class DerivedAnalysis : public DerivedAnalysisBase {
   initialize(const std::map<AnalysisName, const AnalysisResult *> &Map) final {
     auto lookup = [&Map](const AnalysisName &Name) -> const AnalysisResult * {
       auto It = Map.find(Name);
-      if (It == Map.end())
+      if (It == Map.end()) {
         llvm_unreachable("dependency missing from DepResults map; "
                          "dependency graph is not topologically sorted");
+      }
       return It->second;
     };
     return initialize(*static_cast<const DepResultTs *>(
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index 33a7124fbacf8..0e5470495ec23 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -60,20 +60,6 @@ class WPASuite {
     return Data.find(Name) != Data.end();
   }
 
-  /// Returns a reference to the result for \p ResultT, or an error if absent.
-  template <typename ResultT> [[nodiscard]] llvm::Expected<ResultT &> get() {
-    static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
-                  "ResultT must derive from AnalysisResult");
-    static_assert(HasAnalysisName<ResultT>::value,
-                  "ResultT must have a static analysisName() method");
-
-    auto Result = get(ResultT::analysisName());
-    if (!Result) {
-      return Result.takeError();
-    }
-    return static_cast<ResultT &>(*Result);
-  }
-
   /// Returns a const reference to the result for \p ResultT, or an error if
   /// absent.
   template <typename ResultT>
@@ -90,18 +76,6 @@ class WPASuite {
     return static_cast<const ResultT &>(*Result);
   }
 
-  /// Returns a reference to the result for \p Name, or an error if absent.
-  [[nodiscard]] llvm::Expected<AnalysisResult &> get(AnalysisName Name) {
-    auto It = Data.find(Name);
-    if (It == Data.end()) {
-      return ErrorBuilder::create(std::errc::invalid_argument,
-                                  "no result for analysis '{0}' in WPASuite",
-                                  Name.str())
-          .build();
-    }
-    return *It->second;
-  }
-
   /// Returns a const reference to the result for \p Name, or an error if
   /// absent.
   [[nodiscard]] llvm::Expected<const AnalysisResult &>

>From 01e8017f8d57812c38db1f7ce09e1b85b57e0d7c Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 18:41:34 -0700
Subject: [PATCH 06/30] Fix

---
 .../Core/Analysis/AnalysisDriver.h            | 19 ++++---
 .../Core/Analysis/AnalysisRegistry.h          |  8 +--
 .../Core/Analysis/AnalysisDriver.cpp          | 57 +++++++++----------
 .../Core/Analysis/AnalysisRegistry.cpp        |  7 ++-
 .../Analysis/AnalysisDriverTest.cpp           | 13 +++--
 5 files changed, 55 insertions(+), 49 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
index 3ac0d64e7de46..c17cd584f2434 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
@@ -15,7 +15,6 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
 #include "llvm/ADT/ArrayRef.h"
@@ -25,13 +24,18 @@
 
 namespace clang::ssaf {
 
+class AnalysisBase;
+class SummaryAnalysisBase;
+class DerivedAnalysisBase;
+
 /// Orchestrates whole-program analysis over an LUSummary.
 ///
 /// Three run() patterns are supported:
-///   - run() &&        -- all registered analyses; silently skips any whose
-///                        entity data is absent or whose dependency was
-///                        skipped. Requires an rvalue driver because this
-///                        exhausts the LUSummary.
+///   - run() &&        -- all registered analyses in topological dependency
+///                        order. Returns an error if any registered analysis
+///                        has no matching entity data in the LUSummary.
+///                        Requires an rvalue driver because this exhausts the
+///                        LUSummary.
 ///   - run(names)      -- named subset plus transitive dependencies; returns
 ///                        Expected and fails if any listed name has no
 ///                        registered analysis or missing entity data.
@@ -41,7 +45,8 @@ class AnalysisDriver final {
   explicit AnalysisDriver(std::unique_ptr<LUSummary> LU);
 
   /// Runs all registered analyses in topological dependency order.
-  /// Silently skips analyses with absent entity data or skipped dependencies.
+  /// Returns an error if any registered analysis has no matching entity data
+  /// in the LUSummary.
   ///
   /// Requires an rvalue driver (std::move(Driver).run()) because this
   /// exhausts all remaining LUSummary data.
@@ -69,7 +74,7 @@ class AnalysisDriver final {
   /// dependencies) and returns them in topological order via a single DFS.
   /// Reports an error on unregistered names or cycles.
   static llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
-  sortTopologically(llvm::ArrayRef<AnalysisName> Roots);
+  sort(llvm::ArrayRef<AnalysisName> Roots);
 
   /// Executes a topologically-sorted analysis list and returns a WPASuite.
   /// \p IdTable is moved into the returned WPASuite.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
index f51d7845b36e9..33734870218b0 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -26,9 +26,9 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "llvm/Support/Error.h"
 #include "llvm/Support/Registry.h"
 #include <memory>
-#include <optional>
 #include <string>
 #include <vector>
 
@@ -81,9 +81,9 @@ class AnalysisRegistry {
   /// Returns the names of all registered analyses.
   static const std::vector<AnalysisName> &names();
 
-  /// Instantiates the analysis registered under \p Name, or returns
-  /// std::nullopt if no such analysis is registered.
-  static std::optional<std::unique_ptr<AnalysisBase>>
+  /// Instantiates the analysis registered under \p Name, or returns an error
+  /// if no such analysis is registered.
+  static llvm::Expected<std::unique_ptr<AnalysisBase>>
   instantiate(llvm::StringRef Name);
 
 private:
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index 41fd55ce88c6b..b010583a03d4e 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -23,59 +23,45 @@ AnalysisDriver::AnalysisDriver(std::unique_ptr<LUSummary> LU)
     : LU(std::move(LU)) {}
 
 llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
-AnalysisDriver::sortTopologically(llvm::ArrayRef<AnalysisName> Roots) {
+AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
   struct Visitor {
     enum class State { Unvisited, Visiting, Visited };
 
     std::map<AnalysisName, State> Marks;
-    std::map<AnalysisName, std::unique_ptr<AnalysisBase>> Analyses;
     std::vector<std::unique_ptr<AnalysisBase>> Result;
 
-    State getState(const AnalysisName &Name) {
-      auto MarkIt = Marks.find(Name);
-      return MarkIt != Marks.end() ? MarkIt->second : State::Unvisited;
-    }
-
-    void setState(const AnalysisName &Name, State S) { Marks[Name] = S; }
-
     llvm::Error visit(const AnalysisName &Name) {
-      State S = getState(Name);
-
-      if (S == State::Visited) {
+      auto It = Marks.find(Name);
+      switch (It != Marks.end() ? It->second : State::Unvisited) {
+      case State::Visited:
         return llvm::Error::success();
-      }
 
-      if (S == State::Visiting) {
+      case State::Visiting:
         return ErrorBuilder::create(std::errc::invalid_argument,
                                     "cycle detected involving analysis '{0}'",
                                     Name)
             .build();
-      }
 
-      if (S == State::Unvisited) {
-        setState(Name, State::Visiting);
+      case State::Unvisited: {
+        Marks[Name] = State::Visiting;
 
         auto V = AnalysisRegistry::instantiate(Name.str());
         if (!V) {
-          return ErrorBuilder::create(std::errc::invalid_argument,
-                                      "no analysis registered for '{0}'", Name)
-              .build();
+          return V.takeError();
         }
 
-        const auto &Deps = (*V)->dependencyNames();
-        Analyses[Name] = std::move(*V);
-
-        for (const auto &Dep : Deps) {
+        auto Analysis = std::move(*V);
+        for (const auto &Dep : Analysis->dependencyNames()) {
           if (auto Err = visit(Dep)) {
             return Err;
           }
         }
 
-        setState(Name, State::Visited);
-        Result.push_back(std::move(Analyses[Name]));
-        Analyses.erase(Name);
+        Marks[Name] = State::Visited;
+        Result.push_back(std::move(Analysis));
         return llvm::Error::success();
       }
+      }
       llvm_unreachable("unhandled State");
     }
   };
@@ -165,18 +151,23 @@ AnalysisDriver::execute(EntityIdTable IdTable,
   Suite.IdTable = std::move(IdTable);
 
   for (auto &V : Sorted) {
-    if (V->TheKind == AnalysisBase::Kind::Summary) {
+    switch (V->TheKind) {
+    case AnalysisBase::Kind::Summary: {
       auto SA = std::unique_ptr<SummaryAnalysisBase>(
           static_cast<SummaryAnalysisBase *>(V.release()));
       if (auto Err = executeSummaryAnalysis(std::move(SA), Suite)) {
         return std::move(Err);
       }
-    } else {
+      break;
+    }
+    case AnalysisBase::Kind::Derived: {
       auto DA = std::unique_ptr<DerivedAnalysisBase>(
           static_cast<DerivedAnalysisBase *>(V.release()));
       if (auto Err = executeDerivedAnalysis(std::move(DA), Suite)) {
         return std::move(Err);
       }
+      break;
+    }
     }
   }
 
@@ -184,12 +175,16 @@ AnalysisDriver::execute(EntityIdTable IdTable,
 }
 
 llvm::Expected<WPASuite> AnalysisDriver::run() && {
-  return run(AnalysisRegistry::names());
+  auto ExpectedSorted = sort(AnalysisRegistry::names());
+  if (!ExpectedSorted) {
+    return ExpectedSorted.takeError();
+  }
+  return execute(std::move(LU->IdTable), std::move(*ExpectedSorted));
 }
 
 llvm::Expected<WPASuite>
 AnalysisDriver::run(llvm::ArrayRef<AnalysisName> Names) {
-  auto ExpectedSorted = sortTopologically(Names);
+  auto ExpectedSorted = sort(Names);
   if (!ExpectedSorted) {
     return ExpectedSorted.takeError();
   }
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
index aac05fdb08453..e9c5208aa7be6 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
@@ -7,6 +7,7 @@
 //===----------------------------------------------------------------------===//
 
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "llvm/ADT/STLExtras.h"
 
 using namespace clang;
@@ -26,12 +27,14 @@ const std::vector<AnalysisName> &AnalysisRegistry::names() {
   return analysisNames;
 }
 
-std::optional<std::unique_ptr<AnalysisBase>>
+llvm::Expected<std::unique_ptr<AnalysisBase>>
 AnalysisRegistry::instantiate(llvm::StringRef Name) {
   for (const auto &Entry : RegistryT::entries()) {
     if (Entry.getName() == Name) {
       return std::unique_ptr<AnalysisBase>(Entry.instantiate());
     }
   }
-  return std::nullopt;
+  return ErrorBuilder::create(std::errc::invalid_argument,
+                              "no analysis registered for '{0}'", Name)
+      .build();
 }
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
index 22554764adf80..8e9c34e081403 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -234,11 +234,14 @@ TEST(AnalysisRegistryTest, AnalysisIsRegistered) {
 }
 
 TEST(AnalysisRegistryTest, AnalysisCanBeInstantiated) {
-  EXPECT_FALSE(
-      AnalysisRegistry::instantiate("AnalysisNonExisting").has_value());
-  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis1").has_value());
-  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis2").has_value());
-  EXPECT_TRUE(AnalysisRegistry::instantiate("Analysis4").has_value());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("AnalysisNonExisting"),
+                       llvm::Failed());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis1"),
+                       llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis2"),
+                       llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis4"),
+                       llvm::Succeeded());
 }
 
 // run() — processes all registered analyses present in the LUSummary.

>From 84d650f704fbb0860e95e82540ef90939ab5ee24 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 18:52:28 -0700
Subject: [PATCH 07/30] Fix

---
 clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
index 7951e77e7c10b..5ab085c0ef07e 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
@@ -3,10 +3,10 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangScalableStaticAnalysisFrameworkCore
-  ASTEntityMapping.cpp
   Analysis/AnalysisDriver.cpp
   Analysis/AnalysisName.cpp
   Analysis/AnalysisRegistry.cpp
+  ASTEntityMapping.cpp
   EntityLinker/EntityLinker.cpp
   Model/BuildNamespace.cpp
   Model/EntityId.cpp

>From 3b3ed414e272ded7e807f4a3f10aa0dbbd2dd17e Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 19:06:36 -0700
Subject: [PATCH 08/30] Fix

---
 .../Core/Analysis/DerivedAnalysis.h                         | 6 ++++--
 .../Core/Analysis/SummaryAnalysis.h                         | 6 ++++--
 .../Core/Analysis/WPASuite.h                                | 4 ++--
 3 files changed, 10 insertions(+), 6 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index e8c2f2fb8c188..446778f2fd513 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -28,6 +28,7 @@
 namespace clang::ssaf {
 
 class AnalysisDriver;
+class AnalysisRegistry;
 
 /// Type-erased base for derived analyses. Known to AnalysisDriver.
 ///
@@ -81,13 +82,14 @@ class DerivedAnalysis : public DerivedAnalysisBase {
   static_assert((HasAnalysisName<DepResultTs>::value && ...),
                 "Every DepResultT must have a static analysisName() method");
 
+  friend class AnalysisRegistry;
+  using ResultType = ResultT;
+
   std::unique_ptr<ResultT> Result;
 
 public:
   DerivedAnalysis() : Result(std::make_unique<ResultT>()) {}
 
-  using ResultType = ResultT;
-
   /// Used by AnalysisRegistry::Add to derive the registry entry name.
   AnalysisName analysisName() const final { return ResultT::analysisName(); }
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
index 138e0e4754b5e..6d6892305d217 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -27,6 +27,7 @@
 namespace clang::ssaf {
 
 class AnalysisDriver;
+class AnalysisRegistry;
 
 /// Type-erased base for summary analyses. Known to AnalysisDriver.
 ///
@@ -78,13 +79,14 @@ class SummaryAnalysis : public SummaryAnalysisBase {
   static_assert(std::is_base_of_v<EntitySummary, EntitySummaryT>,
                 "EntitySummaryT must derive from EntitySummary");
 
+  friend class AnalysisRegistry;
+  using ResultType = ResultT;
+
   std::unique_ptr<ResultT> Result;
 
 public:
   SummaryAnalysis() : Result(std::make_unique<ResultT>()) {}
 
-  using ResultType = ResultT;
-
   /// Used by AnalysisRegistry::Add to derive the registry entry name.
   AnalysisName analysisName() const final { return ResultT::analysisName(); }
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index 0e5470495ec23..ce2d334a15071 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -7,7 +7,7 @@
 //===----------------------------------------------------------------------===//
 //
 // The value returned by AnalysisDriver::run(). Bundles the EntityIdTable
-// (moved from the LUSummary) with the analysis results keyed by AnalysisName.
+// with the analysis results keyed by AnalysisName.
 //
 //===----------------------------------------------------------------------===//
 
@@ -42,7 +42,7 @@ class WPASuite {
 
 public:
   /// Returns the EntityIdTable that maps EntityId values to their symbolic
-  /// names. Moved from the LUSummary during AnalysisDriver::run().
+  /// names.
   const EntityIdTable &idTable() const { return IdTable; }
 
   /// Returns true if a result for \p ResultT is present.

>From e440a667154c8226c81a0d112c17020676f53d67 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 20:44:39 -0700
Subject: [PATCH 09/30] Fix

---
 .../Core/Analysis/AnalysisDriver.cpp          |  18 +-
 .../Analysis/AnalysisDriverTest.cpp           | 186 ++++++++++++++++--
 2 files changed, 188 insertions(+), 16 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index b010583a03d4e..d510632cf4278 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -11,6 +11,7 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/ErrorHandling.h"
 #include <map>
@@ -28,8 +29,18 @@ AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
     enum class State { Unvisited, Visiting, Visited };
 
     std::map<AnalysisName, State> Marks;
+    std::vector<AnalysisName> Path;
     std::vector<std::unique_ptr<AnalysisBase>> Result;
 
+    std::string formatCycle(const AnalysisName &CycleEntry) const {
+      auto CycleBegin = llvm::find(Path, CycleEntry);
+      std::string Cycle;
+      llvm::raw_string_ostream OS(Cycle);
+      llvm::interleave(llvm::make_range(CycleBegin, Path.end()), OS, " -> ");
+      OS << " -> " << CycleEntry;
+      return Cycle;
+    }
+
     llvm::Error visit(const AnalysisName &Name) {
       auto It = Marks.find(Name);
       switch (It != Marks.end() ? It->second : State::Unvisited) {
@@ -38,26 +49,29 @@ AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
 
       case State::Visiting:
         return ErrorBuilder::create(std::errc::invalid_argument,
-                                    "cycle detected involving analysis '{0}'",
-                                    Name)
+                                    "cycle detected: {0}", formatCycle(Name))
             .build();
 
       case State::Unvisited: {
         Marks[Name] = State::Visiting;
+        Path.push_back(Name);
 
         auto V = AnalysisRegistry::instantiate(Name.str());
         if (!V) {
+          Path.pop_back();
           return V.takeError();
         }
 
         auto Analysis = std::move(*V);
         for (const auto &Dep : Analysis->dependencyNames()) {
           if (auto Err = visit(Dep)) {
+            Path.pop_back();
             return Err;
           }
         }
 
         Marks[Name] = State::Visited;
+        Path.pop_back();
         Result.push_back(std::move(Analysis));
         return llvm::Error::success();
       }
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
index 8e9c34e081403..f2bab5b40d914 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -11,6 +11,7 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
@@ -23,6 +24,7 @@
 #include "llvm/Testing/Support/Error.h"
 #include "gtest/gtest.h"
 #include <memory>
+#include <string>
 #include <utility>
 #include <vector>
 
@@ -85,6 +87,7 @@ class Analysis1Result final : public AnalysisResult {
 public:
   static AnalysisName analysisName() { return AnalysisName("Analysis1"); }
   std::vector<std::pair<EntityId, int>> Entries;
+  bool WasInitialized = false;
   bool WasFinalized = false;
 };
 
@@ -92,6 +95,7 @@ class Analysis2Result final : public AnalysisResult {
 public:
   static AnalysisName analysisName() { return AnalysisName("Analysis2"); }
   std::vector<std::pair<EntityId, int>> Entries;
+  bool WasInitialized = false;
   bool WasFinalized = false;
 };
 
@@ -108,9 +112,36 @@ class Analysis4Result final : public AnalysisResult {
 public:
   static AnalysisName analysisName() { return AnalysisName("Analysis4"); }
   std::vector<std::pair<EntityId, int>> Entries;
+  bool WasInitialized = false;
   bool WasFinalized = false;
 };
 
+// Analysis5 is a derived analysis that depends on Analysis1, Analysis2, and
+// Analysis4. It verifies that the driver passes dependency results to
+// initialize() and that the initialize/step/finalize lifecycle is respected.
+class Analysis5Result final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("Analysis5"); }
+  std::vector<std::string> CallSequence;
+  std::vector<std::pair<EntityId, int>> Analysis1Entries;
+  std::vector<std::pair<EntityId, int>> Analysis2Entries;
+  std::vector<std::pair<EntityId, int>> Analysis4Entries;
+};
+
+// CycleA and CycleB form a dependency cycle (CycleA → CycleB → CycleA).
+// Registered solely to exercise cycle detection in AnalysisDriver::sort().
+// initialize() and step() are unreachable stubs — the cycle is caught before
+// any analysis executes.
+class CycleAResult final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("CycleA"); }
+};
+
+class CycleBResult final : public AnalysisResult {
+public:
+  static AnalysisName analysisName() { return AnalysisName("CycleB"); }
+};
+
 // ---------------------------------------------------------------------------
 // Analysis destruction flags (reset in SetUp)
 // ---------------------------------------------------------------------------
@@ -118,6 +149,7 @@ class Analysis4Result final : public AnalysisResult {
 static bool Analysis1WasDestroyed = false;
 static bool Analysis2WasDestroyed = false;
 static bool Analysis4WasDestroyed = false;
+static bool Analysis5WasDestroyed = false;
 
 // ---------------------------------------------------------------------------
 // Analyses
@@ -128,6 +160,11 @@ class Analysis1 final
 public:
   ~Analysis1() { Analysis1WasDestroyed = true; }
 
+  llvm::Error initialize() override {
+    result().WasInitialized = true;
+    return llvm::Error::success();
+  }
+
   llvm::Error add(EntityId Id, const Analysis1EntitySummary &S) override {
     result().Entries.push_back({Id, S.InstanceId});
     return llvm::Error::success();
@@ -146,6 +183,11 @@ class Analysis2 final
 public:
   ~Analysis2() { Analysis2WasDestroyed = true; }
 
+  llvm::Error initialize() override {
+    result().WasInitialized = true;
+    return llvm::Error::success();
+  }
+
   llvm::Error add(EntityId Id, const Analysis2EntitySummary &S) override {
     result().Entries.push_back({Id, S.InstanceId});
     return llvm::Error::success();
@@ -159,11 +201,18 @@ class Analysis2 final
 
 static AnalysisRegistry::Add<Analysis2> RegAnalysis2("Analysis for Analysis2");
 
+// No Analysis3 or registration for Analysis3.
+
 class Analysis4 final
     : public SummaryAnalysis<Analysis4Result, Analysis4EntitySummary> {
 public:
   ~Analysis4() { Analysis4WasDestroyed = true; }
 
+  llvm::Error initialize() override {
+    result().WasInitialized = true;
+    return llvm::Error::success();
+  }
+
   llvm::Error add(EntityId Id, const Analysis4EntitySummary &S) override {
     result().Entries.push_back({Id, S.InstanceId});
     return llvm::Error::success();
@@ -177,6 +226,56 @@ class Analysis4 final
 
 static AnalysisRegistry::Add<Analysis4> RegAnalysis4("Analysis for Analysis4");
 
+class Analysis5 final
+    : public DerivedAnalysis<Analysis5Result, Analysis1Result, Analysis2Result,
+                             Analysis4Result> {
+  int StepCount = 0;
+
+public:
+  ~Analysis5() { Analysis5WasDestroyed = true; }
+
+  llvm::Error initialize(const Analysis1Result &R1, const Analysis2Result &R2,
+                         const Analysis4Result &R4) override {
+    result().CallSequence.push_back("initialize");
+    result().Analysis1Entries = R1.Entries;
+    result().Analysis2Entries = R2.Entries;
+    result().Analysis4Entries = R4.Entries;
+    return llvm::Error::success();
+  }
+
+  llvm::Expected<bool> step() override {
+    result().CallSequence.push_back("step");
+    return ++StepCount < 2;
+  }
+
+  llvm::Error finalize() override {
+    result().CallSequence.push_back("finalize");
+    return llvm::Error::success();
+  }
+};
+
+static AnalysisRegistry::Add<Analysis5> RegAnalysis5("Analysis for Analysis5");
+
+class CycleA final : public DerivedAnalysis<CycleAResult, CycleBResult> {
+public:
+  llvm::Error initialize(const CycleBResult &) override {
+    return llvm::Error::success();
+  }
+  llvm::Expected<bool> step() override { return false; }
+};
+
+static AnalysisRegistry::Add<CycleA> RegCycleA("Cyclic analysis A (test only)");
+
+class CycleB final : public DerivedAnalysis<CycleBResult, CycleAResult> {
+public:
+  llvm::Error initialize(const CycleAResult &) override {
+    return llvm::Error::success();
+  }
+  llvm::Expected<bool> step() override { return false; }
+};
+
+static AnalysisRegistry::Add<CycleB> RegCycleB("Cyclic analysis B (test only)");
+
 // ---------------------------------------------------------------------------
 // Fixture
 // ---------------------------------------------------------------------------
@@ -191,6 +290,7 @@ class AnalysisDriverTest : public TestFixture {
     Analysis1WasDestroyed = false;
     Analysis2WasDestroyed = false;
     Analysis4WasDestroyed = false;
+    Analysis5WasDestroyed = false;
   }
 
   std::unique_ptr<LUSummary> makeLUSummary() {
@@ -227,25 +327,36 @@ class AnalysisDriverTest : public TestFixture {
 // ---------------------------------------------------------------------------
 
 TEST(AnalysisRegistryTest, AnalysisIsRegistered) {
-  EXPECT_FALSE(AnalysisRegistry::contains("AnalysisNonExisting"));
   EXPECT_TRUE(AnalysisRegistry::contains("Analysis1"));
   EXPECT_TRUE(AnalysisRegistry::contains("Analysis2"));
+  EXPECT_FALSE(AnalysisRegistry::contains("Analysis3"));
   EXPECT_TRUE(AnalysisRegistry::contains("Analysis4"));
+  EXPECT_TRUE(AnalysisRegistry::contains("Analysis5"));
+  EXPECT_TRUE(AnalysisRegistry::contains("CycleA"));
+  EXPECT_TRUE(AnalysisRegistry::contains("CycleB"));
 }
 
 TEST(AnalysisRegistryTest, AnalysisCanBeInstantiated) {
   EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("AnalysisNonExisting"),
-                       llvm::Failed());
+                       llvm::FailedWithMessage(
+                           "no analysis registered for 'AnalysisNonExisting'"));
   EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis1"),
                        llvm::Succeeded());
   EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis2"),
                        llvm::Succeeded());
   EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis4"),
                        llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis5"),
+                       llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("CycleA"),
+                       llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("CycleB"),
+                       llvm::Succeeded());
 }
 
-// run() — processes all registered analyses present in the LUSummary.
-// Silently skips data whose analysis is unregistered (Analysis3).
+// run<T...>() — processes the non-cyclic analyses in topological order.
+// CycleA and CycleB are excluded because they form a cycle; run() && would
+// error on them, so the type-safe subset overload is used here instead.
 TEST_F(AnalysisDriverTest, RunAll) {
   auto LU = makeLUSummary();
   const auto E1 = addEntity(*LU, "Entity1");
@@ -263,7 +374,8 @@ TEST_F(AnalysisDriverTest, RunAll) {
   (void)insertSummary<Analysis3EntitySummary>(*LU, "Analysis3", E1);
 
   AnalysisDriver Driver(std::move(LU));
-  auto WPAOrErr = std::move(Driver).run();
+  auto WPAOrErr = Driver.run<Analysis1Result, Analysis2Result, Analysis4Result,
+                             Analysis5Result>();
   ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
 
   {
@@ -272,6 +384,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_EQ(R1OrErr->Entries.size(), 2u);
     EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
     EXPECT_TRUE(hasEntry(R1OrErr->Entries, E2, s1b));
+    EXPECT_TRUE(R1OrErr->WasInitialized);
     EXPECT_TRUE(R1OrErr->WasFinalized);
     EXPECT_TRUE(Analysis1WasDestroyed);
   }
@@ -282,6 +395,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_EQ(R2OrErr->Entries.size(), 2u);
     EXPECT_TRUE(hasEntry(R2OrErr->Entries, E2, s2a));
     EXPECT_TRUE(hasEntry(R2OrErr->Entries, E3, s2b));
+    EXPECT_TRUE(R2OrErr->WasInitialized);
     EXPECT_TRUE(R2OrErr->WasFinalized);
     EXPECT_TRUE(Analysis2WasDestroyed);
   }
@@ -291,12 +405,32 @@ TEST_F(AnalysisDriverTest, RunAll) {
     ASSERT_THAT_EXPECTED(R4OrErr, llvm::Succeeded());
     EXPECT_EQ(R4OrErr->Entries.size(), 1u);
     EXPECT_TRUE(hasEntry(R4OrErr->Entries, E4, s4a));
+    EXPECT_TRUE(R4OrErr->WasInitialized);
     EXPECT_TRUE(R4OrErr->WasFinalized);
     EXPECT_TRUE(Analysis4WasDestroyed);
   }
 
+  {
+    auto R5OrErr = WPAOrErr->get<Analysis5Result>();
+    ASSERT_THAT_EXPECTED(R5OrErr, llvm::Succeeded());
+    EXPECT_EQ(
+        R5OrErr->CallSequence,
+        (std::vector<std::string>{"initialize", "step", "step", "finalize"}));
+    EXPECT_EQ(R5OrErr->Analysis1Entries.size(), 2u);
+    EXPECT_TRUE(hasEntry(R5OrErr->Analysis1Entries, E1, s1a));
+    EXPECT_TRUE(hasEntry(R5OrErr->Analysis1Entries, E2, s1b));
+    EXPECT_EQ(R5OrErr->Analysis2Entries.size(), 2u);
+    EXPECT_TRUE(hasEntry(R5OrErr->Analysis2Entries, E2, s2a));
+    EXPECT_TRUE(hasEntry(R5OrErr->Analysis2Entries, E3, s2b));
+    EXPECT_EQ(R5OrErr->Analysis4Entries.size(), 1u);
+    EXPECT_TRUE(hasEntry(R5OrErr->Analysis4Entries, E4, s4a));
+    EXPECT_TRUE(Analysis5WasDestroyed);
+  }
+
   // Unregistered analysis — not present in WPA.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis3Result>(), llvm::Failed());
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis3Result>(),
+                       llvm::FailedWithMessage(
+                           "no result for analysis 'Analysis3' in WPASuite"));
 }
 
 // run(names) — processes only the analyses for the given names.
@@ -317,10 +451,13 @@ TEST_F(AnalysisDriverTest, RunByName) {
   ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded());
   EXPECT_EQ(R1OrErr->Entries.size(), 1u);
   EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
+  EXPECT_TRUE(R1OrErr->WasInitialized);
   EXPECT_TRUE(R1OrErr->WasFinalized);
 
   // Analysis2 was not requested — not present even though data exists.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(), llvm::Failed());
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(),
+                       llvm::FailedWithMessage(
+                           "no result for analysis 'Analysis2' in WPASuite"));
 }
 
 // run(names) — error when a requested name has no data in LUSummary.
@@ -328,7 +465,9 @@ TEST_F(AnalysisDriverTest, RunByNameErrorMissingData) {
   auto LU = makeLUSummary();
   AnalysisDriver Driver(std::move(LU));
 
-  EXPECT_THAT_EXPECTED(Driver.run({AnalysisName("Analysis1")}), llvm::Failed());
+  EXPECT_THAT_EXPECTED(
+      Driver.run({AnalysisName("Analysis1")}),
+      llvm::FailedWithMessage("no data for analysis 'Analysis1' in LUSummary"));
 }
 
 // run(names) — error when a requested name has no registered analysis.
@@ -340,7 +479,9 @@ TEST_F(AnalysisDriverTest, RunByNameErrorMissingAnalysis) {
   AnalysisDriver Driver(std::move(LU));
 
   // Analysis3 has data but no registered analysis.
-  EXPECT_THAT_EXPECTED(Driver.run({AnalysisName("Analysis3")}), llvm::Failed());
+  EXPECT_THAT_EXPECTED(
+      Driver.run({AnalysisName("Analysis3")}),
+      llvm::FailedWithMessage("no analysis registered for 'Analysis3'"));
 }
 
 // run<ResultTs...>() — type-safe subset.
@@ -361,10 +502,13 @@ TEST_F(AnalysisDriverTest, RunByType) {
   ASSERT_THAT_EXPECTED(R1OrErr, llvm::Succeeded());
   EXPECT_EQ(R1OrErr->Entries.size(), 1u);
   EXPECT_TRUE(hasEntry(R1OrErr->Entries, E1, s1a));
+  EXPECT_TRUE(R1OrErr->WasInitialized);
   EXPECT_TRUE(R1OrErr->WasFinalized);
 
   // Analysis2 was not requested — not present even though data exists.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(), llvm::Failed());
+  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(),
+                       llvm::FailedWithMessage(
+                           "no result for analysis 'Analysis2' in WPASuite"));
 }
 
 // run<ResultTs...>() — error when a requested type has no data in LUSummary.
@@ -372,7 +516,9 @@ TEST_F(AnalysisDriverTest, RunByTypeErrorMissingData) {
   auto LU = makeLUSummary();
   AnalysisDriver Driver(std::move(LU));
 
-  EXPECT_THAT_EXPECTED(Driver.run<Analysis1Result>(), llvm::Failed());
+  EXPECT_THAT_EXPECTED(
+      Driver.run<Analysis1Result>(),
+      llvm::FailedWithMessage("no data for analysis 'Analysis1' in LUSummary"));
 }
 
 // contains() — present entries return true; absent entries return false.
@@ -380,14 +526,26 @@ TEST_F(AnalysisDriverTest, Contains) {
   auto LU = makeLUSummary();
   const auto E1 = addEntity(*LU, "Entity1");
   insertSummary<Analysis1EntitySummary>(*LU, "Analysis1", E1);
+  insertSummary<Analysis2EntitySummary>(*LU, "Analysis2", E1);
   insertSummary<Analysis4EntitySummary>(*LU, "Analysis4", E1);
 
   AnalysisDriver Driver(std::move(LU));
-  auto WPAOrErr = std::move(Driver).run();
+  auto WPAOrErr = Driver.run<Analysis1Result, Analysis2Result, Analysis4Result,
+                             Analysis5Result>();
   ASSERT_THAT_EXPECTED(WPAOrErr, llvm::Succeeded());
-
   EXPECT_TRUE(WPAOrErr->contains<Analysis1Result>());
-  EXPECT_FALSE(WPAOrErr->contains<Analysis2Result>());
+  // Analysis3 has no registered analysis — never present in WPA.
+  EXPECT_FALSE(WPAOrErr->contains<Analysis3Result>());
+}
+
+// run() && — errors when the registry contains a dependency cycle.
+TEST_F(AnalysisDriverTest, CycleDetected) {
+  auto LU = makeLUSummary();
+  AnalysisDriver Driver(std::move(LU));
+  EXPECT_THAT_EXPECTED(
+      std::move(Driver).run(),
+      llvm::FailedWithMessage("cycle detected: AnalysisName(CycleA) -> "
+                              "AnalysisName(CycleB) -> AnalysisName(CycleA)"));
 }
 
 } // namespace

>From 0d8c983591d11f32bdff9bfe8a8713dcc02e352f Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 21:23:40 -0700
Subject: [PATCH 10/30] Fix

---
 .../Core/Analysis/AnalysisRegistry.h                   |  9 +++++++--
 .../Core/Analysis/AnalysisRegistry.cpp                 | 10 +++++++---
 2 files changed, 14 insertions(+), 5 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
index 33734870218b0..4ed5999bda477 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -64,7 +64,7 @@ class AnalysisRegistry {
       if (contains(Name)) {
         ErrorBuilder::fatal("duplicate analysis registration for '{0}'", Name);
       }
-      analysisNames.push_back(AnalysisT::ResultType::analysisName());
+      getAnalysisNames().push_back(AnalysisT::ResultType::analysisName());
     }
 
     Add(const Add &) = delete;
@@ -87,7 +87,12 @@ class AnalysisRegistry {
   instantiate(llvm::StringRef Name);
 
 private:
-  static std::vector<AnalysisName> analysisNames;
+  /// Returns the global list of registered analysis names.
+  ///
+  /// Uses a function-local static to avoid static initialization order
+  /// fiasco: Add<T> objects in other translation units may push names before
+  /// a plain static data member could be constructed.
+  static std::vector<AnalysisName> &getAnalysisNames();
 };
 
 } // namespace clang::ssaf
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
index e9c5208aa7be6..91e95fa8b2379 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
@@ -17,14 +17,18 @@ using RegistryT = llvm::Registry<AnalysisBase>;
 
 LLVM_INSTANTIATE_REGISTRY(RegistryT)
 
-std::vector<AnalysisName> AnalysisRegistry::analysisNames;
+std::vector<AnalysisName> &AnalysisRegistry::getAnalysisNames() {
+  static std::vector<AnalysisName> Names;
+  return Names;
+}
 
 bool AnalysisRegistry::contains(llvm::StringRef Name) {
-  return llvm::is_contained(analysisNames, AnalysisName(std::string(Name)));
+  return llvm::is_contained(getAnalysisNames(),
+                            AnalysisName(std::string(Name)));
 }
 
 const std::vector<AnalysisName> &AnalysisRegistry::names() {
-  return analysisNames;
+  return getAnalysisNames();
 }
 
 llvm::Expected<std::unique_ptr<AnalysisBase>>

>From 5e2fd99672ccac872a924bd60e765d91ab290881 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 21:50:01 -0700
Subject: [PATCH 11/30] More fixes

---
 .../Core/Analysis/DerivedAnalysis.h                        | 7 ++++---
 .../Core/EntityLinker/LUSummary.h                          | 2 +-
 .../Core/Analysis/AnalysisDriver.cpp                       | 4 ++--
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index 446778f2fd513..7066f3451a3d0 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -19,8 +19,8 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "llvm/Support/Error.h"
-#include "llvm/Support/ErrorHandling.h"
 #include <map>
 #include <memory>
 #include <vector>
@@ -124,8 +124,9 @@ class DerivedAnalysis : public DerivedAnalysisBase {
     auto lookup = [&Map](const AnalysisName &Name) -> const AnalysisResult * {
       auto It = Map.find(Name);
       if (It == Map.end()) {
-        llvm_unreachable("dependency missing from DepResults map; "
-                         "dependency graph is not topologically sorted");
+        ErrorBuilder::fatal("dependency '{0}' missing from DepResults map; "
+                            "dependency graph is not topologically sorted",
+                            Name);
       }
       return It->second;
     };
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
index 44e7504009bee..a36002006430c 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h
@@ -31,10 +31,10 @@ namespace clang::ssaf {
 /// together. It contains deduplicated entities with their linkage information
 /// and the merged entity summaries.
 class LUSummary {
+  friend class AnalysisDriver;
   friend class LUSummaryConsumer;
   friend class SerializationFormat;
   friend class TestFixture;
-  friend class AnalysisDriver;
 
   NestedBuildNamespace LUNamespace;
 
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index d510632cf4278..9572304d8d8c5 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -96,7 +96,7 @@ llvm::Error AnalysisDriver::executeSummaryAnalysis(
   if (DataIt == LU->Data.end()) {
     return ErrorBuilder::create(std::errc::invalid_argument,
                                 "no data for analysis '{0}' in LUSummary",
-                                Summary->analysisName().str())
+                                Summary->analysisName())
         .build();
   }
 
@@ -129,7 +129,7 @@ llvm::Error AnalysisDriver::executeDerivedAnalysis(
     if (It == Suite.Data.end()) {
       ErrorBuilder::fatal("missing dependency '{0}' for analysis '{1}': "
                           "dependency graph is not topologically sorted",
-                          DepName.str(), Derived->analysisName().str());
+                          DepName, Derived->analysisName());
     }
     DepMap[DepName] = It->second.get();
   }

>From ede4e3fee0ee5a401b2cf3c8fba78d04890d624e Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Mon, 16 Mar 2026 22:14:29 -0700
Subject: [PATCH 12/30] Even more fixes

---
 .../Analysis/AnalysisDriverTest.cpp                         | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
index f2bab5b40d914..2fdc752df1760 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -467,7 +467,8 @@ TEST_F(AnalysisDriverTest, RunByNameErrorMissingData) {
 
   EXPECT_THAT_EXPECTED(
       Driver.run({AnalysisName("Analysis1")}),
-      llvm::FailedWithMessage("no data for analysis 'Analysis1' in LUSummary"));
+      llvm::FailedWithMessage(
+          "no data for analysis 'AnalysisName(Analysis1)' in LUSummary"));
 }
 
 // run(names) — error when a requested name has no registered analysis.
@@ -518,7 +519,8 @@ TEST_F(AnalysisDriverTest, RunByTypeErrorMissingData) {
 
   EXPECT_THAT_EXPECTED(
       Driver.run<Analysis1Result>(),
-      llvm::FailedWithMessage("no data for analysis 'Analysis1' in LUSummary"));
+      llvm::FailedWithMessage(
+          "no data for analysis 'AnalysisName(Analysis1)' in LUSummary"));
 }
 
 // contains() — present entries return true; absent entries return false.

>From 0ab38581538844bad825681c997341a574e35591 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Tue, 17 Mar 2026 15:58:14 -0700
Subject: [PATCH 13/30] Balazs

---
 .../Core/Analysis/AnalysisBase.h              | 11 ++-
 .../Core/Analysis/AnalysisDriver.h            | 23 +++---
 .../Core/Analysis/AnalysisName.h              |  2 +-
 .../Core/Analysis/AnalysisRegistry.h          |  2 +-
 .../Core/Analysis/AnalysisResult.h            |  2 +-
 .../Core/Analysis/AnalysisTraits.h            |  2 +-
 .../Core/Analysis/DerivedAnalysis.h           |  9 +--
 .../Core/Analysis/SummaryAnalysis.h           | 10 +--
 .../Core/Analysis/WPASuite.h                  |  2 +-
 .../SSAFBuiltinForceLinker.h                  |  5 ++
 .../Core/Analysis/AnalysisDriver.cpp          | 80 +++++++++----------
 .../Core/Analysis/AnalysisRegistry.cpp        |  2 +
 12 files changed, 72 insertions(+), 78 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
index 29c46f3c2e544..595aa1c9286b5 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
@@ -1,4 +1,4 @@
-//===- AnalysisBase.h -----------------------------------------------------===//
+//===- AnalysisBase.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.
@@ -21,8 +21,9 @@
 namespace clang::ssaf {
 
 class AnalysisDriver;
-class SummaryAnalysisBase;
+class AnalysisResult;
 class DerivedAnalysisBase;
+class SummaryAnalysisBase;
 
 /// Minimal common base for both analysis kinds.
 ///
@@ -30,8 +31,8 @@ class DerivedAnalysisBase;
 /// DerivedAnalysis<...> instead.
 class AnalysisBase {
   friend class AnalysisDriver;
-  friend class SummaryAnalysisBase;
   friend class DerivedAnalysisBase;
+  friend class SummaryAnalysisBase;
 
   enum class Kind { Summary, Derived };
   Kind TheKind;
@@ -48,6 +49,10 @@ class AnalysisBase {
 
   /// AnalysisNames of all AnalysisResult dependencies.
   virtual const std::vector<AnalysisName> &dependencyNames() const = 0;
+
+  /// Transfers ownership of the built result. Called once after finalize().
+  /// The rvalue ref-qualifier enforces single use.
+  virtual std::unique_ptr<AnalysisResult> result() && = 0;
 };
 
 } // namespace clang::ssaf
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
index c17cd584f2434..cb26051d1f106 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
@@ -1,4 +1,4 @@
-//===- AnalysisDriver.h ---------------------------------------------------===//
+//===- AnalysisDriver.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.
@@ -25,8 +25,8 @@
 namespace clang::ssaf {
 
 class AnalysisBase;
-class SummaryAnalysisBase;
 class DerivedAnalysisBase;
+class SummaryAnalysisBase;
 
 /// Orchestrates whole-program analysis over an LUSummary.
 ///
@@ -59,11 +59,12 @@ class AnalysisDriver final {
   /// LUSummary. The EntityIdTable is copied (not moved) so the driver remains
   /// usable for subsequent calls.
   [[nodiscard]] llvm::Expected<WPASuite>
-  run(llvm::ArrayRef<AnalysisName> Names);
+  run(llvm::ArrayRef<AnalysisName> Names) const;
 
   /// Type-safe variant of run(names). Derives names from
   /// ResultTs::analysisName().
-  template <typename... ResultTs> [[nodiscard]] llvm::Expected<WPASuite> run() {
+  template <typename... ResultTs>
+  [[nodiscard]] llvm::Expected<WPASuite> run() const {
     return run({ResultTs::analysisName()...});
   }
 
@@ -74,21 +75,19 @@ class AnalysisDriver final {
   /// dependencies) and returns them in topological order via a single DFS.
   /// Reports an error on unregistered names or cycles.
   static llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
-  sort(llvm::ArrayRef<AnalysisName> Roots);
+  toposort(llvm::ArrayRef<AnalysisName> Roots);
 
   /// Executes a topologically-sorted analysis list and returns a WPASuite.
   /// \p IdTable is moved into the returned WPASuite.
   llvm::Expected<WPASuite>
   execute(EntityIdTable IdTable,
-          std::vector<std::unique_ptr<AnalysisBase>> Sorted);
+          llvm::ArrayRef<std::unique_ptr<AnalysisBase>> Sorted) const;
 
-  llvm::Error
-  executeSummaryAnalysis(std::unique_ptr<SummaryAnalysisBase> Summary,
-                         WPASuite &Suite);
+  llvm::Error executeSummaryAnalysis(SummaryAnalysisBase &Summary,
+                                     WPASuite &Suite) const;
 
-  llvm::Error
-  executeDerivedAnalysis(std::unique_ptr<DerivedAnalysisBase> Derived,
-                         WPASuite &Suite);
+  llvm::Error executeDerivedAnalysis(DerivedAnalysisBase &Derived,
+                                     WPASuite &Suite) const;
 };
 
 } // namespace clang::ssaf
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
index 73ba96ccc594e..7c074fa5695fe 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
@@ -1,4 +1,4 @@
-//===- AnalysisName.h -----------------------------------------------------===//
+//===- AnalysisName.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.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
index 4ed5999bda477..cf275597afc1a 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -1,4 +1,4 @@
-//===- AnalysisRegistry.h -------------------------------------------------===//
+//===- AnalysisRegistry.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.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
index 7ac2a9ad7db6a..97ac3760bfbfa 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
@@ -1,4 +1,4 @@
-//===- AnalysisResult.h ---------------------------------------------------===//
+//===- AnalysisResult.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.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
index ef6a5a56d990a..d7fb37e03115f 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
@@ -1,4 +1,4 @@
-//===- AnalysisTraits.h ---------------------------------------------------===//
+//===- AnalysisTraits.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.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index 7066f3451a3d0..fe2ce9368dae4 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -1,4 +1,4 @@
-//===- DerivedAnalysis.h --------------------------------------------------===//
+//===- DerivedAnalysis.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.
@@ -56,9 +56,6 @@ class DerivedAnalysisBase : public AnalysisBase {
 
   /// Called after the step() loop converges. Default is a no-op.
   virtual llvm::Error finalize() { return llvm::Error::success(); }
-
-  /// Transfers ownership of the computed result. Called once after finalize().
-  virtual std::unique_ptr<AnalysisResult> result() && = 0;
 };
 
 /// Typed intermediate that concrete derived analyses inherit from.
@@ -85,11 +82,9 @@ class DerivedAnalysis : public DerivedAnalysisBase {
   friend class AnalysisRegistry;
   using ResultType = ResultT;
 
-  std::unique_ptr<ResultT> Result;
+  std::unique_ptr<ResultT> Result = std::make_unique<ResultT>();
 
 public:
-  DerivedAnalysis() : Result(std::make_unique<ResultT>()) {}
-
   /// Used by AnalysisRegistry::Add to derive the registry entry name.
   AnalysisName analysisName() const final { return ResultT::analysisName(); }
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
index 6d6892305d217..226427a0091da 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -1,4 +1,4 @@
-//===- SummaryAnalysis.h --------------------------------------------------===//
+//===- SummaryAnalysis.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.
@@ -56,10 +56,6 @@ class SummaryAnalysisBase : public AnalysisBase {
 
   /// Called after all entities have been processed. Default is a no-op.
   virtual llvm::Error finalize() { return llvm::Error::success(); }
-
-  /// Transfers ownership of the built result. Called once after finalize().
-  /// The rvalue ref-qualifier enforces single use.
-  virtual std::unique_ptr<AnalysisResult> result() && = 0;
 };
 
 /// Typed intermediate that concrete summary analyses inherit from.
@@ -82,11 +78,9 @@ class SummaryAnalysis : public SummaryAnalysisBase {
   friend class AnalysisRegistry;
   using ResultType = ResultT;
 
-  std::unique_ptr<ResultT> Result;
+  std::unique_ptr<ResultT> Result = std::make_unique<ResultT>();
 
 public:
-  SummaryAnalysis() : Result(std::make_unique<ResultT>()) {}
-
   /// Used by AnalysisRegistry::Add to derive the registry entry name.
   AnalysisName analysisName() const final { return ResultT::analysisName(); }
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index ce2d334a15071..a4c6be09e08f0 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -1,4 +1,4 @@
-//===- WPASuite.h ---------------------------------------------------------===//
+//===- WPASuite.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.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h b/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
index 5f201487ca1fe..2f144b92a1a94 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
@@ -25,4 +25,9 @@ extern volatile int SSAFJSONFormatAnchorSource;
 [[maybe_unused]] static int SSAFJSONFormatAnchorDestination =
     SSAFJSONFormatAnchorSource;
 
+// This anchor is used to force the linker to link the AnalysisRegistry.
+extern volatile int SSAFAnalysisRegistryAnchorSource;
+[[maybe_unused]] static int SSAFAnalysisRegistryAnchorDestination =
+    SSAFAnalysisRegistryAnchorSource;
+
 #endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SSAFBUILTINFORCELINKER_H
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index 9572304d8d8c5..d04b8e640e515 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -24,7 +24,7 @@ AnalysisDriver::AnalysisDriver(std::unique_ptr<LUSummary> LU)
     : LU(std::move(LU)) {}
 
 llvm::Expected<std::vector<std::unique_ptr<AnalysisBase>>>
-AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
+AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) {
   struct Visitor {
     enum class State { Unvisited, Visiting, Visited };
 
@@ -42,8 +42,9 @@ AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
     }
 
     llvm::Error visit(const AnalysisName &Name) {
-      auto It = Marks.find(Name);
-      switch (It != Marks.end() ? It->second : State::Unvisited) {
+      auto [It, _] = Marks.emplace(Name, State::Unvisited);
+
+      switch (It->second) {
       case State::Visited:
         return llvm::Error::success();
 
@@ -53,26 +54,25 @@ AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
             .build();
 
       case State::Unvisited: {
-        Marks[Name] = State::Visiting;
+        It->second = State::Visiting;
         Path.push_back(Name);
 
         auto V = AnalysisRegistry::instantiate(Name.str());
         if (!V) {
-          Path.pop_back();
           return V.takeError();
         }
 
         auto Analysis = std::move(*V);
         for (const auto &Dep : Analysis->dependencyNames()) {
           if (auto Err = visit(Dep)) {
-            Path.pop_back();
             return Err;
           }
         }
 
-        Marks[Name] = State::Visited;
+        It->second = State::Visited;
         Path.pop_back();
         Result.push_back(std::move(Analysis));
+
         return llvm::Error::success();
       }
       }
@@ -89,57 +89,54 @@ AnalysisDriver::sort(llvm::ArrayRef<AnalysisName> Roots) {
   return std::move(V.Result);
 }
 
-llvm::Error AnalysisDriver::executeSummaryAnalysis(
-    std::unique_ptr<SummaryAnalysisBase> Summary, WPASuite &Suite) {
-  SummaryName SN = Summary->summaryName();
+llvm::Error AnalysisDriver::executeSummaryAnalysis(SummaryAnalysisBase &Summary,
+                                                   WPASuite &Suite) const {
+  SummaryName SN = Summary.summaryName();
   auto DataIt = LU->Data.find(SN);
   if (DataIt == LU->Data.end()) {
     return ErrorBuilder::create(std::errc::invalid_argument,
                                 "no data for analysis '{0}' in LUSummary",
-                                Summary->analysisName())
+                                Summary.analysisName())
         .build();
   }
 
-  if (auto Err = Summary->initialize()) {
+  if (auto Err = Summary.initialize()) {
     return Err;
   }
 
   for (auto &[Id, EntitySummary] : DataIt->second) {
-    if (auto Err = Summary->add(Id, *EntitySummary)) {
+    if (auto Err = Summary.add(Id, *EntitySummary)) {
       return Err;
     }
   }
 
-  if (auto Err = Summary->finalize()) {
+  if (auto Err = Summary.finalize()) {
     return Err;
   }
 
-  AnalysisName Name = Summary->analysisName();
-  Suite.Data.emplace(Name, std::move(*Summary).result());
-
   return llvm::Error::success();
 }
 
-llvm::Error AnalysisDriver::executeDerivedAnalysis(
-    std::unique_ptr<DerivedAnalysisBase> Derived, WPASuite &Suite) {
+llvm::Error AnalysisDriver::executeDerivedAnalysis(DerivedAnalysisBase &Derived,
+                                                   WPASuite &Suite) const {
   std::map<AnalysisName, const AnalysisResult *> DepMap;
 
-  for (const auto &DepName : Derived->dependencyNames()) {
+  for (const auto &DepName : Derived.dependencyNames()) {
     auto It = Suite.Data.find(DepName);
     if (It == Suite.Data.end()) {
       ErrorBuilder::fatal("missing dependency '{0}' for analysis '{1}': "
                           "dependency graph is not topologically sorted",
-                          DepName, Derived->analysisName());
+                          DepName, Derived.analysisName());
     }
     DepMap[DepName] = It->second.get();
   }
 
-  if (auto Err = Derived->initialize(DepMap)) {
+  if (auto Err = Derived.initialize(DepMap)) {
     return Err;
   }
 
   while (true) {
-    auto StepOrErr = Derived->step();
+    auto StepOrErr = Derived.step();
     if (!StepOrErr) {
       return StepOrErr.takeError();
     }
@@ -148,60 +145,57 @@ llvm::Error AnalysisDriver::executeDerivedAnalysis(
     }
   }
 
-  if (auto Err = Derived->finalize()) {
+  if (auto Err = Derived.finalize()) {
     return Err;
   }
 
-  AnalysisName Name = Derived->analysisName();
-  Suite.Data.emplace(Name, std::move(*Derived).result());
-
   return llvm::Error::success();
 }
 
-llvm::Expected<WPASuite>
-AnalysisDriver::execute(EntityIdTable IdTable,
-                        std::vector<std::unique_ptr<AnalysisBase>> Sorted) {
+llvm::Expected<WPASuite> AnalysisDriver::execute(
+    EntityIdTable IdTable,
+    llvm::ArrayRef<std::unique_ptr<AnalysisBase>> Sorted) const {
   WPASuite Suite;
   Suite.IdTable = std::move(IdTable);
 
-  for (auto &V : Sorted) {
-    switch (V->TheKind) {
+  for (auto &Analysis : Sorted) {
+    switch (Analysis->TheKind) {
     case AnalysisBase::Kind::Summary: {
-      auto SA = std::unique_ptr<SummaryAnalysisBase>(
-          static_cast<SummaryAnalysisBase *>(V.release()));
-      if (auto Err = executeSummaryAnalysis(std::move(SA), Suite)) {
+      SummaryAnalysisBase &SA = static_cast<SummaryAnalysisBase &>(*Analysis);
+      if (auto Err = executeSummaryAnalysis(SA, Suite)) {
         return std::move(Err);
       }
       break;
     }
     case AnalysisBase::Kind::Derived: {
-      auto DA = std::unique_ptr<DerivedAnalysisBase>(
-          static_cast<DerivedAnalysisBase *>(V.release()));
-      if (auto Err = executeDerivedAnalysis(std::move(DA), Suite)) {
+      DerivedAnalysisBase &DA = static_cast<DerivedAnalysisBase &>(*Analysis);
+      if (auto Err = executeDerivedAnalysis(DA, Suite)) {
         return std::move(Err);
       }
       break;
     }
     }
+    AnalysisName Name = Analysis->analysisName();
+    Suite.Data.emplace(Name, std::move(*Analysis).result());
   }
 
   return Suite;
 }
 
 llvm::Expected<WPASuite> AnalysisDriver::run() && {
-  auto ExpectedSorted = sort(AnalysisRegistry::names());
+  auto ExpectedSorted = toposort(AnalysisRegistry::names());
   if (!ExpectedSorted) {
     return ExpectedSorted.takeError();
   }
-  return execute(std::move(LU->IdTable), std::move(*ExpectedSorted));
+  return execute(std::move(LU->IdTable), *ExpectedSorted);
 }
 
 llvm::Expected<WPASuite>
-AnalysisDriver::run(llvm::ArrayRef<AnalysisName> Names) {
-  auto ExpectedSorted = sort(Names);
+AnalysisDriver::run(llvm::ArrayRef<AnalysisName> Names) const {
+  auto ExpectedSorted = toposort(Names);
   if (!ExpectedSorted) {
     return ExpectedSorted.takeError();
   }
 
-  return execute(LU->IdTable, std::move(*ExpectedSorted));
+  return execute(LU->IdTable, *ExpectedSorted);
 }
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
index 91e95fa8b2379..d39c47335f1bb 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
@@ -15,6 +15,8 @@ using namespace ssaf;
 
 using RegistryT = llvm::Registry<AnalysisBase>;
 
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int SSAFAnalysisRegistryAnchorSource = 0;
 LLVM_INSTANTIATE_REGISTRY(RegistryT)
 
 std::vector<AnalysisName> &AnalysisRegistry::getAnalysisNames() {

>From fb389e30508c30cb8c829ad01cc77a0ed24a8e16 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 19 Mar 2026 11:54:23 -0700
Subject: [PATCH 14/30] Balazs 2

---
 .../Core/Analysis/AnalysisRegistry.h          | 13 +++-
 .../Core/Analysis/AnalysisTraits.h            |  3 +
 .../Core/Analysis/DerivedAnalysis.h           |  8 +--
 .../Core/Analysis/SummaryAnalysis.h           |  6 +-
 .../Core/Analysis/WPASuite.h                  |  4 +-
 .../Core/Analysis/AnalysisDriver.cpp          | 19 ++++--
 .../Analysis/AnalysisDriverTest.cpp           | 68 +++++++++----------
 7 files changed, 70 insertions(+), 51 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
index cf275597afc1a..2199d2213d7ac 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
@@ -8,12 +8,21 @@
 //
 // Unified registry for both SummaryAnalysis and DerivedAnalysis subclasses.
 //
-// To register an analysis, add a static Add<AnalysisT> in its translation
-// unit:
+// To register an analysis, add a static Add<AnalysisT> and an anchor source
+// in its translation unit, then add the matching anchor destination to the
+// relevant force-linker header:
 //
+//   // MyAnalysis.cpp
 //   static AnalysisRegistry::Add<MyAnalysis>
 //       Registered("One-line description of MyAnalysis");
 //
+//   volatile int SSAFMyAnalysisAnchorSource = 0;
+//
+//   // SSAFBuiltinForceLinker.h (or the relevant force-linker header)
+//   extern volatile int SSAFMyAnalysisAnchorSource;
+//   [[maybe_unused]] static int SSAFMyAnalysisAnchorDestination =
+//       SSAFMyAnalysisAnchorSource;
+//
 // The registry entry name is derived automatically from
 // MyAnalysis::analysisName(), so name-mismatch bugs are impossible.
 //
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
index d7fb37e03115f..c7e0a77c85785 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
@@ -28,6 +28,9 @@ template <typename T>
 struct HasAnalysisName<T, std::void_t<decltype(T::analysisName())>>
     : std::is_same<decltype(T::analysisName()), AnalysisName> {};
 
+template <typename T>
+inline constexpr bool HasAnalysisName_v = HasAnalysisName<T>::value;
+
 } // namespace clang::ssaf
 
 #endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
index fe2ce9368dae4..edc5429e3b49c 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
@@ -50,8 +50,8 @@ class DerivedAnalysisBase : public AnalysisBase {
   virtual llvm::Error initialize(
       const std::map<AnalysisName, const AnalysisResult *> &DepResults) = 0;
 
-  /// Performs one pass. Returns true if another pass is needed; false when
-  /// converged.
+  /// Performs one pass.
+  /// Returns true if another pass is needed; false when converged.
   virtual llvm::Expected<bool> step() = 0;
 
   /// Called after the step() loop converges. Default is a no-op.
@@ -72,11 +72,11 @@ template <typename ResultT, typename... DepResultTs>
 class DerivedAnalysis : public DerivedAnalysisBase {
   static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
                 "ResultT must derive from AnalysisResult");
-  static_assert(HasAnalysisName<ResultT>::value,
+  static_assert(HasAnalysisName_v<ResultT>,
                 "ResultT must have a static analysisName() method");
   static_assert((std::is_base_of_v<AnalysisResult, DepResultTs> && ...),
                 "Every DepResultT must derive from AnalysisResult");
-  static_assert((HasAnalysisName<DepResultTs>::value && ...),
+  static_assert((HasAnalysisName_v<DepResultTs> && ...),
                 "Every DepResultT must have a static analysisName() method");
 
   friend class AnalysisRegistry;
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
index 226427a0091da..ca9123c1d1d61 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
@@ -70,7 +70,7 @@ template <typename ResultT, typename EntitySummaryT>
 class SummaryAnalysis : public SummaryAnalysisBase {
   static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
                 "ResultT must derive from AnalysisResult");
-  static_assert(HasAnalysisName<ResultT>::value,
+  static_assert(HasAnalysisName_v<ResultT>,
                 "ResultT must have a static analysisName() method");
   static_assert(std::is_base_of_v<EntitySummary, EntitySummaryT>,
                 "EntitySummaryT must derive from EntitySummary");
@@ -99,8 +99,8 @@ class SummaryAnalysis : public SummaryAnalysisBase {
   /// Called once per matching entity. Implement to accumulate data.
   virtual llvm::Error add(EntityId Id, const EntitySummaryT &Summary) = 0;
 
-  /// Called after all entities have been processed. Override for
-  /// post-processing.
+  /// Called after all entities have been processed.
+  /// Override for post-processing.
   virtual llvm::Error finalize() override { return llvm::Error::success(); }
 
 protected:
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
index a4c6be09e08f0..4fc23826fe200 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
@@ -49,7 +49,7 @@ class WPASuite {
   template <typename ResultT> [[nodiscard]] bool contains() const {
     static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
                   "ResultT must derive from AnalysisResult");
-    static_assert(HasAnalysisName<ResultT>::value,
+    static_assert(HasAnalysisName_v<ResultT>,
                   "ResultT must have a static analysisName() method");
 
     return contains(ResultT::analysisName());
@@ -66,7 +66,7 @@ class WPASuite {
   [[nodiscard]] llvm::Expected<const ResultT &> get() const {
     static_assert(std::is_base_of_v<AnalysisResult, ResultT>,
                   "ResultT must derive from AnalysisResult");
-    static_assert(HasAnalysisName<ResultT>::value,
+    static_assert(HasAnalysisName_v<ResultT>,
                   "ResultT must have a static analysisName() method");
 
     auto Result = get(ResultT::analysisName());
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
index d04b8e640e515..e3b358ef2c432 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
@@ -32,6 +32,11 @@ AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) {
     std::vector<AnalysisName> Path;
     std::vector<std::unique_ptr<AnalysisBase>> Result;
 
+    explicit Visitor(size_t N) {
+      Path.reserve(N);
+      Result.reserve(N);
+    }
+
     std::string formatCycle(const AnalysisName &CycleEntry) const {
       auto CycleBegin = llvm::find(Path, CycleEntry);
       std::string Cycle;
@@ -57,18 +62,24 @@ AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) {
         It->second = State::Visiting;
         Path.push_back(Name);
 
-        auto V = AnalysisRegistry::instantiate(Name.str());
+        llvm::Expected<std::unique_ptr<AnalysisBase>> V =
+            AnalysisRegistry::instantiate(Name.str());
         if (!V) {
           return V.takeError();
         }
 
-        auto Analysis = std::move(*V);
+        // Unwrap for convenience to avoid the noise of dereferencing an
+        // Expected on every subsequent access.
+        std::unique_ptr<AnalysisBase> Analysis = std::move(*V);
+
         for (const auto &Dep : Analysis->dependencyNames()) {
           if (auto Err = visit(Dep)) {
             return Err;
           }
         }
 
+        // std::map iterators are not invalidated by insertions, so It remains
+        // valid after recursive visit() calls that insert new entries.
         It->second = State::Visited;
         Path.pop_back();
         Result.push_back(std::move(Analysis));
@@ -80,7 +91,7 @@ AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) {
     }
   };
 
-  Visitor V;
+  Visitor V(Roots.size());
   for (const auto &Root : Roots) {
     if (auto Err = V.visit(Root)) {
       return std::move(Err);
@@ -176,7 +187,7 @@ llvm::Expected<WPASuite> AnalysisDriver::execute(
     }
     }
     AnalysisName Name = Analysis->analysisName();
-    Suite.Data.emplace(Name, std::move(*Analysis).result());
+    Suite.Data.emplace(std::move(Name), std::move(*Analysis).result());
   }
 
   return Suite;
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
index 2fdc752df1760..3f603715c984e 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
@@ -129,8 +129,8 @@ class Analysis5Result final : public AnalysisResult {
 };
 
 // CycleA and CycleB form a dependency cycle (CycleA → CycleB → CycleA).
-// Registered solely to exercise cycle detection in AnalysisDriver::sort().
-// initialize() and step() are unreachable stubs — the cycle is caught before
+// Registered solely to exercise cycle detection in AnalysisDriver::toposort().
+// initialize() and step() are unreachable stubs - the cycle is caught before
 // any analysis executes.
 class CycleAResult final : public AnalysisResult {
 public:
@@ -142,15 +142,6 @@ class CycleBResult final : public AnalysisResult {
   static AnalysisName analysisName() { return AnalysisName("CycleB"); }
 };
 
-// ---------------------------------------------------------------------------
-// Analysis destruction flags (reset in SetUp)
-// ---------------------------------------------------------------------------
-
-static bool Analysis1WasDestroyed = false;
-static bool Analysis2WasDestroyed = false;
-static bool Analysis4WasDestroyed = false;
-static bool Analysis5WasDestroyed = false;
-
 // ---------------------------------------------------------------------------
 // Analyses
 // ---------------------------------------------------------------------------
@@ -158,7 +149,8 @@ static bool Analysis5WasDestroyed = false;
 class Analysis1 final
     : public SummaryAnalysis<Analysis1Result, Analysis1EntitySummary> {
 public:
-  ~Analysis1() { Analysis1WasDestroyed = true; }
+  inline static bool WasDestroyed = false;
+  ~Analysis1() { WasDestroyed = true; }
 
   llvm::Error initialize() override {
     result().WasInitialized = true;
@@ -176,12 +168,17 @@ class Analysis1 final
   }
 };
 
+// These static registrations are safe without SSAFBuiltinTestForceLinker.h
+// because this translation unit is compiled directly into the test binary -
+// the linker cannot dead-strip it, so all static initializers are guaranteed
+// to run.
 static AnalysisRegistry::Add<Analysis1> RegAnalysis1("Analysis for Analysis1");
 
 class Analysis2 final
     : public SummaryAnalysis<Analysis2Result, Analysis2EntitySummary> {
 public:
-  ~Analysis2() { Analysis2WasDestroyed = true; }
+  inline static bool WasDestroyed = false;
+  ~Analysis2() { WasDestroyed = true; }
 
   llvm::Error initialize() override {
     result().WasInitialized = true;
@@ -206,7 +203,8 @@ static AnalysisRegistry::Add<Analysis2> RegAnalysis2("Analysis for Analysis2");
 class Analysis4 final
     : public SummaryAnalysis<Analysis4Result, Analysis4EntitySummary> {
 public:
-  ~Analysis4() { Analysis4WasDestroyed = true; }
+  inline static bool WasDestroyed = false;
+  ~Analysis4() { WasDestroyed = true; }
 
   llvm::Error initialize() override {
     result().WasInitialized = true;
@@ -232,7 +230,8 @@ class Analysis5 final
   int StepCount = 0;
 
 public:
-  ~Analysis5() { Analysis5WasDestroyed = true; }
+  inline static bool WasDestroyed = false;
+  ~Analysis5() { WasDestroyed = true; }
 
   llvm::Error initialize(const Analysis1Result &R1, const Analysis2Result &R2,
                          const Analysis4Result &R4) override {
@@ -287,10 +286,11 @@ class AnalysisDriverTest : public TestFixture {
 
   void SetUp() override {
     NextSummaryInstanceId = 0;
-    Analysis1WasDestroyed = false;
-    Analysis2WasDestroyed = false;
-    Analysis4WasDestroyed = false;
-    Analysis5WasDestroyed = false;
+    Analysis1::WasDestroyed = false;
+    Analysis2::WasDestroyed = false;
+    // No Analysis3 - not registered, so no WasDestroyed flag.
+    Analysis4::WasDestroyed = false;
+    Analysis5::WasDestroyed = false;
   }
 
   std::unique_ptr<LUSummary> makeLUSummary() {
@@ -337,21 +337,17 @@ TEST(AnalysisRegistryTest, AnalysisIsRegistered) {
 }
 
 TEST(AnalysisRegistryTest, AnalysisCanBeInstantiated) {
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("AnalysisNonExisting"),
+  constexpr auto instantiate = AnalysisRegistry::instantiate;
+  EXPECT_THAT_EXPECTED(instantiate("AnalysisNonExisting"),
                        llvm::FailedWithMessage(
                            "no analysis registered for 'AnalysisNonExisting'"));
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis1"),
-                       llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis2"),
-                       llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis4"),
-                       llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("Analysis5"),
-                       llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("CycleA"),
-                       llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(AnalysisRegistry::instantiate("CycleB"),
-                       llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate("Analysis1"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate("Analysis2"), llvm::Succeeded());
+  // No Analysis3 - not registered, so instantiate() would fail.
+  EXPECT_THAT_EXPECTED(instantiate("Analysis4"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate("Analysis5"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate("CycleA"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate("CycleB"), llvm::Succeeded());
 }
 
 // run<T...>() — processes the non-cyclic analyses in topological order.
@@ -386,7 +382,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_TRUE(hasEntry(R1OrErr->Entries, E2, s1b));
     EXPECT_TRUE(R1OrErr->WasInitialized);
     EXPECT_TRUE(R1OrErr->WasFinalized);
-    EXPECT_TRUE(Analysis1WasDestroyed);
+    EXPECT_TRUE(Analysis1::WasDestroyed);
   }
 
   {
@@ -397,7 +393,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_TRUE(hasEntry(R2OrErr->Entries, E3, s2b));
     EXPECT_TRUE(R2OrErr->WasInitialized);
     EXPECT_TRUE(R2OrErr->WasFinalized);
-    EXPECT_TRUE(Analysis2WasDestroyed);
+    EXPECT_TRUE(Analysis2::WasDestroyed);
   }
 
   {
@@ -407,7 +403,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_TRUE(hasEntry(R4OrErr->Entries, E4, s4a));
     EXPECT_TRUE(R4OrErr->WasInitialized);
     EXPECT_TRUE(R4OrErr->WasFinalized);
-    EXPECT_TRUE(Analysis4WasDestroyed);
+    EXPECT_TRUE(Analysis4::WasDestroyed);
   }
 
   {
@@ -424,7 +420,7 @@ TEST_F(AnalysisDriverTest, RunAll) {
     EXPECT_TRUE(hasEntry(R5OrErr->Analysis2Entries, E3, s2b));
     EXPECT_EQ(R5OrErr->Analysis4Entries.size(), 1u);
     EXPECT_TRUE(hasEntry(R5OrErr->Analysis4Entries, E4, s4a));
-    EXPECT_TRUE(Analysis5WasDestroyed);
+    EXPECT_TRUE(Analysis5::WasDestroyed);
   }
 
   // Unregistered analysis — not present in WPA.

>From ebc9ece064fe416256b53e3cab718f438dde24c3 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 19 Mar 2026 12:09:13 -0700
Subject: [PATCH 15/30] I like to move it, move it!

---
 .../Core/Support/FormatProviders.h                 |  2 +-
 .../AnalysisBase.h                                 |  8 ++++----
 .../AnalysisDriver.h                               |  8 ++++----
 .../AnalysisName.h                                 |  6 +++---
 .../AnalysisRegistry.h                             | 12 ++++++------
 .../AnalysisResult.h                               |  6 +++---
 .../AnalysisTraits.h                               |  8 ++++----
 .../DerivedAnalysis.h                              | 14 +++++++-------
 .../SummaryAnalysis.h                              | 12 ++++++------
 .../{Analysis => WholeProgramAnalysis}/WPASuite.h  | 12 ++++++------
 .../Core/CMakeLists.txt                            |  6 +++---
 .../AnalysisDriver.cpp                             |  8 ++++----
 .../AnalysisName.cpp                               |  2 +-
 .../AnalysisRegistry.cpp                           |  2 +-
 .../ScalableStaticAnalysisFramework/CMakeLists.txt |  2 +-
 .../AnalysisDriverTest.cpp                         | 14 +++++++-------
 16 files changed, 61 insertions(+), 61 deletions(-)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisBase.h (82%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisDriver.h (90%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisName.h (85%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisRegistry.h (86%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisResult.h (75%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisTraits.h (75%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/DerivedAnalysis.h (88%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/SummaryAnalysis.h (89%)
 rename clang/include/clang/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/WPASuite.h (85%)
 rename clang/lib/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisDriver.cpp (94%)
 rename clang/lib/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisName.cpp (86%)
 rename clang/lib/ScalableStaticAnalysisFramework/Core/{Analysis => WholeProgramAnalysis}/AnalysisRegistry.cpp (94%)
 rename clang/unittests/ScalableStaticAnalysisFramework/{Analysis => WholeProgramAnalysis}/AnalysisDriverTest.cpp (96%)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
index 6cc816edfd967..437152d43f425 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h
@@ -14,12 +14,12 @@
 #ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 #define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SUPPORT_FORMATPROVIDERS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "llvm/Support/FormatProviders.h"
 #include "llvm/Support/raw_ostream.h"
 
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisBase.h
similarity index 82%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisBase.h
index 595aa1c9286b5..b86a9c5828700 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisBase.h
@@ -12,10 +12,10 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISBASE_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISBASE_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include <vector>
 
 namespace clang::ssaf {
@@ -57,4 +57,4 @@ class AnalysisBase {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISBASE_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISBASE_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h
similarity index 90%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h
index cb26051d1f106..156d8e806bd0f 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h
@@ -12,11 +12,11 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISDRIVER_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISDRIVER_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
 #include "llvm/ADT/ArrayRef.h"
 #include "llvm/Support/Error.h"
 #include <memory>
@@ -92,4 +92,4 @@ class AnalysisDriver final {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISDRIVER_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISDRIVER_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h
similarity index 85%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h
index 7c074fa5695fe..32f76e73b14e0 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h
@@ -11,8 +11,8 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISNAME_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISNAME_H
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/raw_ostream.h"
@@ -46,4 +46,4 @@ llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, const AnalysisName &AN);
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISNAME_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISNAME_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
similarity index 86%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
index 2199d2213d7ac..7ea55e4b3af91 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
@@ -28,13 +28,13 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISREGISTRY_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISREGISTRY_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/Registry.h"
 #include <memory>
@@ -106,4 +106,4 @@ class AnalysisRegistry {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISREGISTRY_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISREGISTRY_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h
similarity index 75%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h
index 97ac3760bfbfa..07d1f0549a9ee 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h
@@ -11,8 +11,8 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISRESULT_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISRESULT_H
 
 namespace clang::ssaf {
 
@@ -27,4 +27,4 @@ class AnalysisResult {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISRESULT_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISRESULT_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h
similarity index 75%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h
index c7e0a77c85785..78df3b35648c2 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h
@@ -10,10 +10,10 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISTRAITS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISTRAITS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include <type_traits>
 
 namespace clang::ssaf {
@@ -33,4 +33,4 @@ inline constexpr bool HasAnalysisName_v = HasAnalysisName<T>::value;
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_ANALYSISTRAITS_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_ANALYSISTRAITS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
similarity index 88%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
index edc5429e3b49c..4eb35262d4625 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
@@ -12,14 +12,14 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_DERIVEDANALYSIS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_DERIVEDANALYSIS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisBase.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h"
 #include "llvm/Support/Error.h"
 #include <map>
 #include <memory>
@@ -137,4 +137,4 @@ class DerivedAnalysis : public DerivedAnalysisBase {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_DERIVEDANALYSIS_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_DERIVEDANALYSIS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h
similarity index 89%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h
index ca9123c1d1d61..31b9e6ae4a6c3 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h
@@ -12,15 +12,15 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_SUMMARYANALYSIS_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_SUMMARYANALYSIS_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisBase.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/EntitySummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisBase.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h"
 #include "llvm/Support/Error.h"
 #include <memory>
 
@@ -125,4 +125,4 @@ class SummaryAnalysis : public SummaryAnalysisBase {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_SUMMARYANALYSIS_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_SUMMARYANALYSIS_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
similarity index 85%
rename from clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
rename to clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
index 4fc23826fe200..b2e9c36d94bd9 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
@@ -11,14 +11,14 @@
 //
 //===----------------------------------------------------------------------===//
 
-#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
-#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
+#ifndef LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_WPASUITE_H
+#define LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_WPASUITE_H
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisTraits.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityIdTable.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisTraits.h"
 #include "llvm/Support/Error.h"
 #include <map>
 #include <memory>
@@ -93,4 +93,4 @@ class WPASuite {
 
 } // namespace clang::ssaf
 
-#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_ANALYSIS_WPASUITE_H
+#endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_WHOLEPROGRAMANALYSIS_WPASUITE_H
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
index 5ab085c0ef07e..8c306163df1a7 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
@@ -3,9 +3,6 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangScalableStaticAnalysisFrameworkCore
-  Analysis/AnalysisDriver.cpp
-  Analysis/AnalysisName.cpp
-  Analysis/AnalysisRegistry.cpp
   ASTEntityMapping.cpp
   EntityLinker/EntityLinker.cpp
   Model/BuildNamespace.cpp
@@ -26,6 +23,9 @@ add_clang_library(clangScalableStaticAnalysisFrameworkCore
   Support/ErrorBuilder.cpp
   TUSummary/ExtractorRegistry.cpp
   TUSummary/TUSummaryBuilder.cpp
+  WholeProgramAnalysis/AnalysisDriver.cpp
+  WholeProgramAnalysis/AnalysisName.cpp
+  WholeProgramAnalysis/AnalysisRegistry.cpp
 
   LINK_LIBS
   clangAST
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
similarity index 94%
rename from clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
rename to clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
index e3b358ef2c432..8d6791f47cfd4 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
@@ -6,11 +6,11 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/ErrorHandling.h"
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.cpp
similarity index 86%
rename from clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp
rename to clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.cpp
index d49f41ab24eb8..9719196ed4d6d 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.cpp
@@ -6,7 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 
 using namespace clang::ssaf;
 
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
similarity index 94%
rename from clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
rename to clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
index d39c47335f1bb..ba91d27f9f168 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
@@ -6,7 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "llvm/ADT/STLExtras.h"
 
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 147131db4fb12..345eed6c5279b 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -1,6 +1,5 @@
 add_distinct_clang_unittest(ClangScalableAnalysisTests
   Analyses/UnsafeBufferUsage/UnsafeBufferUsageTest.cpp
-  Analysis/AnalysisDriverTest.cpp
   ASTEntityMappingTest.cpp
   BuildNamespaceTest.cpp
   EntityIdTableTest.cpp
@@ -24,6 +23,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   SummaryNameTest.cpp
   TestFixture.cpp
   TUSummaryBuilderTest.cpp
+  WholeProgramAnalysis/AnalysisDriverTest.cpp
 
   CLANG_LIBS
   clangAST
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
similarity index 96%
rename from clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
rename to clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
index 3f603715c984e..ee6cc31408c29 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Analysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
@@ -6,20 +6,20 @@
 //
 //===----------------------------------------------------------------------===//
 
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisDriver.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h"
 #include "../TestFixture.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisRegistry.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/DerivedAnalysis.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/SummaryAnalysis.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Analysis/WPASuite.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityId.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/SummaryAnalysis.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Testing/Support/Error.h"
 #include "gtest/gtest.h"

>From 6756efc56cec40a79da9689eba5cfd9de080da96 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 19 Mar 2026 15:20:39 -0700
Subject: [PATCH 16/30] AnalysisRegistry APIs and error messages should accept
 AnalysisName as arguments

---
 .../WholeProgramAnalysis/AnalysisRegistry.h   |  6 +-
 .../Core/WholeProgramAnalysis/WPASuite.h      |  3 +-
 .../WholeProgramAnalysis/AnalysisDriver.cpp   |  2 +-
 .../WholeProgramAnalysis/AnalysisRegistry.cpp |  9 +-
 .../AnalysisDriverTest.cpp                    | 83 +++++++++++--------
 5 files changed, 59 insertions(+), 44 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
index 7ea55e4b3af91..44eabce6c809c 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h
@@ -70,7 +70,7 @@ class AnalysisRegistry {
     explicit Add(llvm::StringRef Desc)
         : Name(AnalysisT::ResultType::analysisName().str().str()),
           Node(Name, Desc) {
-      if (contains(Name)) {
+      if (contains(AnalysisT::ResultType::analysisName())) {
         ErrorBuilder::fatal("duplicate analysis registration for '{0}'", Name);
       }
       getAnalysisNames().push_back(AnalysisT::ResultType::analysisName());
@@ -85,7 +85,7 @@ class AnalysisRegistry {
   };
 
   /// Returns true if an analysis is registered under \p Name.
-  static bool contains(llvm::StringRef Name);
+  static bool contains(const AnalysisName &Name);
 
   /// Returns the names of all registered analyses.
   static const std::vector<AnalysisName> &names();
@@ -93,7 +93,7 @@ class AnalysisRegistry {
   /// Instantiates the analysis registered under \p Name, or returns an error
   /// if no such analysis is registered.
   static llvm::Expected<std::unique_ptr<AnalysisBase>>
-  instantiate(llvm::StringRef Name);
+  instantiate(const AnalysisName &Name);
 
 private:
   /// Returns the global list of registered analysis names.
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
index b2e9c36d94bd9..5a0105fc1f4d9 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
@@ -83,8 +83,7 @@ class WPASuite {
     auto It = Data.find(Name);
     if (It == Data.end()) {
       return ErrorBuilder::create(std::errc::invalid_argument,
-                                  "no result for analysis '{0}' in WPASuite",
-                                  Name.str())
+                                  "no result for '{0}' in WPASuite", Name)
           .build();
     }
     return *It->second;
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
index 8d6791f47cfd4..47eb3f6d4e7e7 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.cpp
@@ -63,7 +63,7 @@ AnalysisDriver::toposort(llvm::ArrayRef<AnalysisName> Roots) {
         Path.push_back(Name);
 
         llvm::Expected<std::unique_ptr<AnalysisBase>> V =
-            AnalysisRegistry::instantiate(Name.str());
+            AnalysisRegistry::instantiate(Name);
         if (!V) {
           return V.takeError();
         }
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
index ba91d27f9f168..8e1ea954d9afd 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.cpp
@@ -24,9 +24,8 @@ std::vector<AnalysisName> &AnalysisRegistry::getAnalysisNames() {
   return Names;
 }
 
-bool AnalysisRegistry::contains(llvm::StringRef Name) {
-  return llvm::is_contained(getAnalysisNames(),
-                            AnalysisName(std::string(Name)));
+bool AnalysisRegistry::contains(const AnalysisName &Name) {
+  return llvm::is_contained(getAnalysisNames(), Name);
 }
 
 const std::vector<AnalysisName> &AnalysisRegistry::names() {
@@ -34,9 +33,9 @@ const std::vector<AnalysisName> &AnalysisRegistry::names() {
 }
 
 llvm::Expected<std::unique_ptr<AnalysisBase>>
-AnalysisRegistry::instantiate(llvm::StringRef Name) {
+AnalysisRegistry::instantiate(const AnalysisName &Name) {
   for (const auto &Entry : RegistryT::entries()) {
-    if (Entry.getName() == Name) {
+    if (Entry.getName() == Name.str()) {
       return std::unique_ptr<AnalysisBase>(Entry.instantiate());
     }
   }
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
index ee6cc31408c29..e206b33d80295 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/WholeProgramAnalysis/AnalysisDriverTest.cpp
@@ -79,13 +79,25 @@ class Analysis4EntitySummary final : public EntitySummary {
   }
 };
 
+// ---------------------------------------------------------------------------
+// Analysis names
+// ---------------------------------------------------------------------------
+
+const AnalysisName Analysis1Name("Analysis1");
+const AnalysisName Analysis2Name("Analysis2");
+const AnalysisName Analysis3Name("Analysis3");
+const AnalysisName Analysis4Name("Analysis4");
+const AnalysisName Analysis5Name("Analysis5");
+const AnalysisName CycleAName("CycleA");
+const AnalysisName CycleBName("CycleB");
+
 // ---------------------------------------------------------------------------
 // Results
 // ---------------------------------------------------------------------------
 
 class Analysis1Result final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("Analysis1"); }
+  static AnalysisName analysisName() { return Analysis1Name; }
   std::vector<std::pair<EntityId, int>> Entries;
   bool WasInitialized = false;
   bool WasFinalized = false;
@@ -93,7 +105,7 @@ class Analysis1Result final : public AnalysisResult {
 
 class Analysis2Result final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("Analysis2"); }
+  static AnalysisName analysisName() { return Analysis2Name; }
   std::vector<std::pair<EntityId, int>> Entries;
   bool WasInitialized = false;
   bool WasFinalized = false;
@@ -103,14 +115,14 @@ class Analysis2Result final : public AnalysisResult {
 // into the LUSummary to verify the driver silently skips it.
 class Analysis3Result final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("Analysis3"); }
+  static AnalysisName analysisName() { return Analysis3Name; }
 };
 
 // Analysis4 has a registered analysis but no data is inserted into the
 // LUSummary, so it is skipped and get() returns nullptr.
 class Analysis4Result final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("Analysis4"); }
+  static AnalysisName analysisName() { return Analysis4Name; }
   std::vector<std::pair<EntityId, int>> Entries;
   bool WasInitialized = false;
   bool WasFinalized = false;
@@ -121,7 +133,7 @@ class Analysis4Result final : public AnalysisResult {
 // initialize() and that the initialize/step/finalize lifecycle is respected.
 class Analysis5Result final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("Analysis5"); }
+  static AnalysisName analysisName() { return Analysis5Name; }
   std::vector<std::string> CallSequence;
   std::vector<std::pair<EntityId, int>> Analysis1Entries;
   std::vector<std::pair<EntityId, int>> Analysis2Entries;
@@ -134,12 +146,12 @@ class Analysis5Result final : public AnalysisResult {
 // any analysis executes.
 class CycleAResult final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("CycleA"); }
+  static AnalysisName analysisName() { return CycleAName; }
 };
 
 class CycleBResult final : public AnalysisResult {
 public:
-  static AnalysisName analysisName() { return AnalysisName("CycleB"); }
+  static AnalysisName analysisName() { return CycleBName; }
 };
 
 // ---------------------------------------------------------------------------
@@ -327,27 +339,28 @@ class AnalysisDriverTest : public TestFixture {
 // ---------------------------------------------------------------------------
 
 TEST(AnalysisRegistryTest, AnalysisIsRegistered) {
-  EXPECT_TRUE(AnalysisRegistry::contains("Analysis1"));
-  EXPECT_TRUE(AnalysisRegistry::contains("Analysis2"));
-  EXPECT_FALSE(AnalysisRegistry::contains("Analysis3"));
-  EXPECT_TRUE(AnalysisRegistry::contains("Analysis4"));
-  EXPECT_TRUE(AnalysisRegistry::contains("Analysis5"));
-  EXPECT_TRUE(AnalysisRegistry::contains("CycleA"));
-  EXPECT_TRUE(AnalysisRegistry::contains("CycleB"));
+  EXPECT_TRUE(AnalysisRegistry::contains(Analysis1Name));
+  EXPECT_TRUE(AnalysisRegistry::contains(Analysis2Name));
+  EXPECT_FALSE(AnalysisRegistry::contains(Analysis3Name));
+  EXPECT_TRUE(AnalysisRegistry::contains(Analysis4Name));
+  EXPECT_TRUE(AnalysisRegistry::contains(Analysis5Name));
+  EXPECT_TRUE(AnalysisRegistry::contains(CycleAName));
+  EXPECT_TRUE(AnalysisRegistry::contains(CycleBName));
 }
 
 TEST(AnalysisRegistryTest, AnalysisCanBeInstantiated) {
   constexpr auto instantiate = AnalysisRegistry::instantiate;
-  EXPECT_THAT_EXPECTED(instantiate("AnalysisNonExisting"),
-                       llvm::FailedWithMessage(
-                           "no analysis registered for 'AnalysisNonExisting'"));
-  EXPECT_THAT_EXPECTED(instantiate("Analysis1"), llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(instantiate("Analysis2"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(
+      instantiate(AnalysisName("AnalysisNonExisting")),
+      llvm::FailedWithMessage(
+          "no analysis registered for 'AnalysisName(AnalysisNonExisting)'"));
+  EXPECT_THAT_EXPECTED(instantiate(Analysis1Name), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate(Analysis2Name), llvm::Succeeded());
   // No Analysis3 - not registered, so instantiate() would fail.
-  EXPECT_THAT_EXPECTED(instantiate("Analysis4"), llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(instantiate("Analysis5"), llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(instantiate("CycleA"), llvm::Succeeded());
-  EXPECT_THAT_EXPECTED(instantiate("CycleB"), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate(Analysis4Name), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate(Analysis5Name), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate(CycleAName), llvm::Succeeded());
+  EXPECT_THAT_EXPECTED(instantiate(CycleBName), llvm::Succeeded());
 }
 
 // run<T...>() — processes the non-cyclic analyses in topological order.
@@ -424,9 +437,10 @@ TEST_F(AnalysisDriverTest, RunAll) {
   }
 
   // Unregistered analysis — not present in WPA.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis3Result>(),
-                       llvm::FailedWithMessage(
-                           "no result for analysis 'Analysis3' in WPASuite"));
+  EXPECT_THAT_EXPECTED(
+      WPAOrErr->get<Analysis3Result>(),
+      llvm::FailedWithMessage(
+          "no result for 'AnalysisName(Analysis3)' in WPASuite"));
 }
 
 // run(names) — processes only the analyses for the given names.
@@ -451,9 +465,10 @@ TEST_F(AnalysisDriverTest, RunByName) {
   EXPECT_TRUE(R1OrErr->WasFinalized);
 
   // Analysis2 was not requested — not present even though data exists.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(),
-                       llvm::FailedWithMessage(
-                           "no result for analysis 'Analysis2' in WPASuite"));
+  EXPECT_THAT_EXPECTED(
+      WPAOrErr->get<Analysis2Result>(),
+      llvm::FailedWithMessage(
+          "no result for 'AnalysisName(Analysis2)' in WPASuite"));
 }
 
 // run(names) — error when a requested name has no data in LUSummary.
@@ -478,7 +493,8 @@ TEST_F(AnalysisDriverTest, RunByNameErrorMissingAnalysis) {
   // Analysis3 has data but no registered analysis.
   EXPECT_THAT_EXPECTED(
       Driver.run({AnalysisName("Analysis3")}),
-      llvm::FailedWithMessage("no analysis registered for 'Analysis3'"));
+      llvm::FailedWithMessage(
+          "no analysis registered for 'AnalysisName(Analysis3)'"));
 }
 
 // run<ResultTs...>() — type-safe subset.
@@ -503,9 +519,10 @@ TEST_F(AnalysisDriverTest, RunByType) {
   EXPECT_TRUE(R1OrErr->WasFinalized);
 
   // Analysis2 was not requested — not present even though data exists.
-  EXPECT_THAT_EXPECTED(WPAOrErr->get<Analysis2Result>(),
-                       llvm::FailedWithMessage(
-                           "no result for analysis 'Analysis2' in WPASuite"));
+  EXPECT_THAT_EXPECTED(
+      WPAOrErr->get<Analysis2Result>(),
+      llvm::FailedWithMessage(
+          "no result for 'AnalysisName(Analysis2)' in WPASuite"));
 }
 
 // run<ResultTs...>() — error when a requested type has no data in LUSummary.

>From 004991a57c1ae62d64b2a04d3a9439405e2cd025 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 18 Mar 2026 16:28:39 -0700
Subject: [PATCH 17/30] Add JSONFormat support for WPASuite

---
 .../Core/Model/PrivateFieldNames.def          |   2 +
 .../Core/Serialization/JSONFormat.h           |  29 +
 .../Core/Serialization/SerializationFormat.h  | 124 +++
 .../Core/WholeProgramAnalysis/WPASuite.h      |   4 +
 .../Core/CMakeLists.txt                       |   1 +
 .../JSONFormat/JSONFormatImpl.cpp             |  14 +
 .../Serialization/JSONFormat/JSONFormatImpl.h |   8 +
 .../Serialization/JSONFormat/WPASuite.cpp     | 209 +++++
 .../CMakeLists.txt                            |   1 +
 .../TUSummaryExtractorFrontendActionTest.cpp  |   9 +
 .../Registries/MockSerializationFormat.cpp    |  10 +
 .../Registries/MockSerializationFormat.h      |   5 +
 .../JSONFormatTest/WPASuiteTest.cpp           | 768 ++++++++++++++++++
 .../TestFixture.h                             |   3 +
 14 files changed, 1187 insertions(+)
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
 create mode 100644 clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def
index 1ee2a809430f4..1c75543062cbc 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def
@@ -42,5 +42,7 @@ FIELD(TUSummaryEncoding, Data)
 FIELD(TUSummaryEncoding, IdTable)
 FIELD(TUSummaryEncoding, LinkageTable)
 FIELD(TUSummaryEncoding, TUNamespace)
+FIELD(WPASuite, Data)
+FIELD(WPASuite, IdTable)
 
 #undef FIELD
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h
index 47b46cbe42698..18dc71585b0fd 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h
@@ -58,6 +58,11 @@ class JSONFormat final : public SerializationFormat {
   llvm::Error writeLUSummaryEncoding(const LUSummaryEncoding &SummaryEncoding,
                                      llvm::StringRef Path) override;
 
+  llvm::Expected<WPASuite> readWPASuite(llvm::StringRef Path) override;
+
+  llvm::Error writeWPASuite(const WPASuite &Suite,
+                            llvm::StringRef Path) override;
+
   void forEachRegisteredAnalysis(
       llvm::function_ref<void(llvm::StringRef Name, llvm::StringRef Desc)>
           Callback) const override;
@@ -74,6 +79,16 @@ class JSONFormat final : public SerializationFormat {
 
   using FormatInfo = FormatInfoEntry<SerializerFn, DeserializerFn>;
 
+  using AnalysisResultSerializerFn =
+      llvm::function_ref<Object(const AnalysisResult &, EntityIdToJSONFn)>;
+  using AnalysisResultDeserializerFn =
+      llvm::function_ref<llvm::Expected<std::unique_ptr<AnalysisResult>>(
+          const Object &, EntityIdFromJSONFn)>;
+
+  using AnalysisResultRegistry =
+      SerializationFormat::AnalysisResultRegistryGenerator<
+          JSONFormat, AnalysisResultSerializerFn, AnalysisResultDeserializerFn>;
+
 private:
   static std::map<SummaryName, FormatInfo> initFormatInfos();
   const std::map<SummaryName, FormatInfo> FormatInfos = initFormatInfos();
@@ -186,6 +201,18 @@ class JSONFormat final : public SerializationFormat {
       const std::map<SummaryName,
                      std::map<EntityId, std::unique_ptr<EntitySummaryEncoding>>>
           &EncodingSummaryDataMap) const;
+
+  llvm::Expected<std::pair<AnalysisName, std::unique_ptr<AnalysisResult>>>
+  analysisResultMapEntryFromJSON(const Object &Entry) const;
+  llvm::Expected<Object> analysisResultMapEntryToJSON(
+      const AnalysisName &Name,
+      const std::unique_ptr<AnalysisResult> &Result) const;
+
+  llvm::Expected<std::map<AnalysisName, std::unique_ptr<AnalysisResult>>>
+  analysisResultMapFromJSON(const Array &ResultsArray) const;
+  llvm::Expected<Array> analysisResultMapToJSON(
+      const std::map<AnalysisName, std::unique_ptr<AnalysisResult>> &Data)
+      const;
 };
 
 } // namespace clang::ssaf
@@ -193,6 +220,8 @@ class JSONFormat final : public SerializationFormat {
 namespace llvm {
 extern template class CLANG_TEMPLATE_ABI
     Registry<clang::ssaf::JSONFormat::FormatInfo>;
+extern template class CLANG_TEMPLATE_ABI
+    Registry<clang::ssaf::JSONFormat::AnalysisResultRegistry::Entry>;
 } // namespace llvm
 
 #endif // LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_CORE_SERIALIZATION_JSONFORMAT_H
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
index 7aebf06a6368e..113e8ca9b1497 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
@@ -19,10 +19,13 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/TUSummaryEncoding.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/BuildNamespace.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
 #include "llvm/ADT/STLFunctionalExtras.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/Error.h"
+#include "llvm/Support/Registry.h"
 
 namespace clang::ssaf {
 
@@ -55,6 +58,11 @@ class SerializationFormat {
   writeLUSummaryEncoding(const LUSummaryEncoding &SummaryEncoding,
                          llvm::StringRef Path) = 0;
 
+  virtual llvm::Expected<WPASuite> readWPASuite(llvm::StringRef Path) = 0;
+
+  virtual llvm::Error writeWPASuite(const WPASuite &Suite,
+                                    llvm::StringRef Path) = 0;
+
   /// Invokes \p Callback once for each analysis that has registered
   /// serialization support for this format.
   virtual void forEachRegisteredAnalysis(
@@ -67,10 +75,126 @@ class SerializationFormat {
 
   static EntityId makeEntityId(const size_t Index) { return EntityId(Index); }
 
+  /// Constructs an empty WPASuite. Bypasses the private default constructor
+  /// so that deserialization code can build a WPASuite incrementally.
+  static WPASuite makeWPASuite() { return WPASuite(); }
+
 #define FIELD(CLASS, FIELD_NAME)                                               \
   static const auto &get##FIELD_NAME(const CLASS &X) { return X.FIELD_NAME; }  \
   static auto &get##FIELD_NAME(CLASS &X) { return X.FIELD_NAME; }
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def"
+
+  /// Generates a per-format plugin registry for analysis result
+  /// serializers and deserializers.
+  ///
+  /// Each concrete format (e.g. JSONFormat) instantiates this template once
+  /// via a \c using alias, then exposes that alias publicly so that analysis
+  /// authors can register (de)serialization support with a single declaration:
+  ///
+  ///   static MyFormat::AnalysisResultRegistryGenerator::Add<MyAnalysisResult>
+  ///       Reg(serializeFn, deserializeFn);
+  ///
+  /// ---
+  /// Design overview
+  /// ---
+  ///
+  /// **Registry isolation via \p FormatT.**
+  /// The underlying store is \c llvm::Registry<Entry>, which is a global
+  /// linked list keyed on the \c Entry type. Because \c Entry is a member of
+  /// this template, each \c (FormatT, SerializerFn, DeserializerFn)
+  /// instantiation produces a distinct \c Entry type and therefore a distinct
+  /// \c llvm::Registry — even if two formats happen to share the same
+  /// serializer/deserializer function signatures. The \p FormatT parameter
+  /// exists solely to provide this isolation; it is otherwise unused inside
+  /// the template body.
+  ///
+  /// **Bridging \c function_ref into \c llvm::Registry.**
+  /// \c llvm::function_ref is a non-owning view of a callable — it cannot be
+  /// stored inside the registry because the registry only keeps nullary
+  /// factories of the form <tt>[]{ return make_unique<ConcreteEntry>(); }</tt>
+  /// that capture no state. Two mechanisms bridge this gap:
+  ///
+  ///   1. *Function-local statics as per-analysis storage.*
+  ///      Inside \c Add<AnalysisResultT>::Add(...), two function-local statics
+  ///      — \c SavedSerialize and \c SavedDeserialize — are initialized from
+  ///      the constructor arguments on the first (and only) call. Because
+  ///      \c Add<T>::Add(...) is a distinct function for each \c T, each
+  ///      analysis type gets its own pair of statics with program lifetime,
+  ///      giving the \c function_ref values a stable home.
+  ///
+  ///   2. *\c ConcreteEntry as a local struct.*
+  ///      \c ConcreteEntry is defined inside the \c Add constructor body so
+  ///      that its default constructor can read \c SavedSerialize and
+  ///      \c SavedDeserialize from the enclosing function scope. When
+  ///      \c llvm::Registry later calls \c E.instantiate() during \c lookup,
+  ///      it invokes \c ConcreteEntry(), which re-wraps those stored values
+  ///      into a fresh \c Entry — reconstructing the \c function_ref from the
+  ///      same stable underlying callables.
+  ///
+  /// **One-time registration via a function-local static \c Reg.**
+  /// \c static typename RegistryT::template Add<ConcreteEntry> Reg(NameStr,"")
+  /// is also a function-local static. C++ guarantees it is initialized exactly
+  /// once, on the first call to \c Add<T>::Add(...). Its constructor appends a
+  /// node to the \c llvm::Registry linked list, associating \c NameStr with
+  /// the \c ConcreteEntry factory. All subsequent calls to \c Add<T>::Add(...)
+  /// hit the \c Registered guard first and abort with a fatal error, making
+  /// duplicate registrations a detectable programmer mistake rather than a
+  /// silent no-op.
+  ///
+  /// **Lookup.**
+  /// \c lookup iterates \c RegistryT::entries() and compares each node's name
+  /// (available directly on the entry node without instantiation) against the
+  /// requested \c AnalysisName. Only the matching node is instantiated via
+  /// \c E.instantiate(), which invokes \c ConcreteEntry() and returns the
+  /// stored \c function_ref pair.
+  template <class FormatT, class SerializerFn, class DeserializerFn>
+  class AnalysisResultRegistryGenerator {
+  public:
+    struct Entry {
+      explicit Entry(SerializerFn Serialize, DeserializerFn Deserialize)
+          : Serialize(Serialize), Deserialize(Deserialize) {}
+      virtual ~Entry() = default;
+      SerializerFn Serialize;
+      DeserializerFn Deserialize;
+    };
+
+    using RegistryT = llvm::Registry<Entry>;
+
+    template <class AnalysisResultT> struct Add {
+      Add(SerializerFn Serialize, DeserializerFn Deserialize) {
+        static bool Registered = false;
+        if (Registered) {
+          ErrorBuilder::fatal("support is already registered for analysis: {0}",
+                              AnalysisResultT::analysisName());
+        }
+        Registered = true;
+        static SerializerFn SavedSerialize = Serialize;
+        static DeserializerFn SavedDeserialize = Deserialize;
+
+        struct ConcreteEntry : Entry {
+          ConcreteEntry() : Entry(SavedSerialize, SavedDeserialize) {}
+        };
+
+        static std::string NameStr =
+            AnalysisResultT::analysisName().str().str();
+        static typename RegistryT::template Add<ConcreteEntry> Reg(NameStr, "");
+      }
+    };
+
+    static llvm::Expected<std::pair<SerializerFn, DeserializerFn>>
+    lookup(const AnalysisName &Name) {
+      for (const auto &E : RegistryT::entries()) {
+        if (E.getName() == Name.str()) {
+          auto Entry = E.instantiate();
+          return std::make_pair(Entry->Serialize, Entry->Deserialize);
+        }
+      }
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  "no support registered for analysis: {0}",
+                                  Name)
+          .build();
+    }
+  };
 };
 
 template <class SerializerFn, class DeserializerFn> struct FormatInfoEntry {
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
index 5a0105fc1f4d9..52c6f9e46297d 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h
@@ -26,6 +26,8 @@
 namespace clang::ssaf {
 
 class AnalysisDriver;
+class SerializationFormat;
+class TestFixture;
 
 /// Bundles the EntityIdTable (moved from the LUSummary) and the analysis
 /// results produced by one AnalysisDriver::run() call, keyed by AnalysisName.
@@ -34,6 +36,8 @@ class AnalysisDriver;
 /// are self-contained in one object.
 class WPASuite {
   friend class AnalysisDriver;
+  friend class SerializationFormat;
+  friend class TestFixture;
 
   EntityIdTable IdTable;
   std::map<AnalysisName, std::unique_ptr<AnalysisResult>> Data;
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
index 8c306163df1a7..83772ceff58bf 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/CMakeLists.txt
@@ -17,6 +17,7 @@ add_clang_library(clangScalableStaticAnalysisFrameworkCore
   Serialization/JSONFormat/LUSummaryEncoding.cpp
   Serialization/JSONFormat/TUSummary.cpp
   Serialization/JSONFormat/TUSummaryEncoding.cpp
+  Serialization/JSONFormat/WPASuite.cpp
   Serialization/SerializationFormatRegistry.cpp
   SummaryData/LUSummaryConsumer.cpp
   SummaryData/SummaryDataBuilderRegistry.cpp
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
index 4072532d4972c..230fed5bac20d 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -13,8 +13,12 @@
 
 // NOLINTNEXTLINE(misc-use-internal-linkage)
 volatile int SSAFJSONFormatAnchorSource = 0;
+
 LLVM_INSTANTIATE_REGISTRY(llvm::Registry<clang::ssaf::JSONFormat::FormatInfo>)
 
+LLVM_INSTANTIATE_REGISTRY(
+    llvm::Registry<clang::ssaf::JSONFormat::AnalysisResultRegistry::Entry>)
+
 static clang::ssaf::SerializationFormatRegistry::Add<clang::ssaf::JSONFormat>
     RegisterJSONFormat("json", "JSON serialization format");
 
@@ -141,6 +145,16 @@ SummaryName summaryNameFromJSON(llvm::StringRef SummaryNameStr) {
 
 llvm::StringRef summaryNameToJSON(const SummaryName &SN) { return SN.str(); }
 
+//----------------------------------------------------------------------------
+// AnalysisName
+//----------------------------------------------------------------------------
+
+AnalysisName analysisNameFromJSON(llvm::StringRef AnalysisNameStr) {
+  return AnalysisName(AnalysisNameStr.str());
+}
+
+llvm::StringRef analysisNameToJSON(const AnalysisName &AN) { return AN.str(); }
+
 //----------------------------------------------------------------------------
 // EntityId
 //----------------------------------------------------------------------------
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
index 2ff1f2ae16192..97154b19bed31 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
@@ -16,6 +16,7 @@
 
 #include "../../ModelStringConversions.h"
 #include "JSONEntitySummaryEncoding.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/EntitySummaryEncoding.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
@@ -139,6 +140,13 @@ llvm::Error writeJSON(Value &&V, llvm::StringRef Path);
 SummaryName summaryNameFromJSON(llvm::StringRef SummaryNameStr);
 llvm::StringRef summaryNameToJSON(const SummaryName &SN);
 
+//----------------------------------------------------------------------------
+// AnalysisName helpers
+//----------------------------------------------------------------------------
+
+AnalysisName analysisNameFromJSON(llvm::StringRef AnalysisNameStr);
+llvm::StringRef analysisNameToJSON(const AnalysisName &AN);
+
 //----------------------------------------------------------------------------
 // BuildNamespaceKind helpers
 //----------------------------------------------------------------------------
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
new file mode 100644
index 0000000000000..f6673d9ef4742
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
@@ -0,0 +1,209 @@
+//===- WPASuite.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 "JSONFormatImpl.h"
+
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+
+namespace clang::ssaf {
+
+//----------------------------------------------------------------------------
+// AnalysisResultMapEntry
+//----------------------------------------------------------------------------
+
+llvm::Expected<std::pair<AnalysisName, std::unique_ptr<AnalysisResult>>>
+JSONFormat::analysisResultMapEntryFromJSON(const Object &Entry) const {
+  auto OptName = Entry.getString("analysis_name");
+  if (!OptName) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadObjectAtField,
+                                "AnalysisName", "analysis_name", "string")
+        .build();
+  }
+
+  AnalysisName Name = analysisNameFromJSON(*OptName);
+
+  auto ExpectedFns = AnalysisResultRegistryGenerator::lookup(Name);
+  if (!ExpectedFns) {
+    return ExpectedFns.takeError();
+  }
+
+  const Object *ResultObj = Entry.getObject("result");
+  if (!ResultObj) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadObjectAtField,
+                                "AnalysisResult", "result", "object")
+        .build();
+  }
+
+  auto ExpectedResult =
+      ExpectedFns->second(*ResultObj, &entityIdFromJSONObject);
+  if (!ExpectedResult) {
+    return ExpectedResult.takeError();
+  }
+
+  return std::make_pair(std::move(Name), std::move(*ExpectedResult));
+}
+
+llvm::Expected<Object> JSONFormat::analysisResultMapEntryToJSON(
+    const AnalysisName &Name,
+    const std::unique_ptr<AnalysisResult> &Result) const {
+  auto ExpectedFns = AnalysisResultRegistryGenerator::lookup(Name);
+  if (!ExpectedFns) {
+    return ExpectedFns.takeError();
+  }
+
+  Object Entry;
+  Entry["analysis_name"] = analysisNameToJSON(Name);
+  Entry["result"] = ExpectedFns->first(*Result, &entityIdToJSONObject);
+  return Entry;
+}
+
+//----------------------------------------------------------------------------
+// AnalysisResultMap
+//----------------------------------------------------------------------------
+
+llvm::Expected<std::map<AnalysisName, std::unique_ptr<AnalysisResult>>>
+JSONFormat::analysisResultMapFromJSON(const Array &ResultsArray) const {
+  std::map<AnalysisName, std::unique_ptr<AnalysisResult>> Results;
+  for (size_t I = 0; I < ResultsArray.size(); ++I) {
+    const Object *Entry = ResultsArray[I].getAsObject();
+    if (!Entry) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedToReadObjectAtIndex,
+                                  "WPA result entry", I, "object")
+          .build();
+    }
+
+    auto ExpectedPair = analysisResultMapEntryFromJSON(*Entry);
+    if (!ExpectedPair) {
+      return ErrorBuilder::wrap(ExpectedPair.takeError())
+          .context(ErrorMessages::ReadingFromIndex, "WPA result entry", I)
+          .build();
+    }
+
+    auto [Name, Result] = std::move(*ExpectedPair);
+    bool Inserted = Results.try_emplace(Name, std::move(Result)).second;
+    if (!Inserted) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedInsertionOnDuplication,
+                                  "WPA result", I, Name)
+          .build();
+    }
+  }
+  return Results;
+}
+
+llvm::Expected<Array> JSONFormat::analysisResultMapToJSON(
+    const std::map<AnalysisName, std::unique_ptr<AnalysisResult>> &Data) const {
+  Array Results;
+  for (const auto &[Name, Result] : Data) {
+    auto ExpectedEntry = analysisResultMapEntryToJSON(Name, Result);
+    if (!ExpectedEntry) {
+      return ExpectedEntry.takeError();
+    }
+    Results.push_back(std::move(*ExpectedEntry));
+  }
+  return Results;
+}
+
+//----------------------------------------------------------------------------
+// WPASuite
+//----------------------------------------------------------------------------
+
+llvm::Expected<WPASuite> JSONFormat::readWPASuite(llvm::StringRef Path) {
+  auto ExpectedJSON = readJSON(Path);
+  if (!ExpectedJSON) {
+    return ErrorBuilder::wrap(ExpectedJSON.takeError())
+        .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+        .build();
+  }
+
+  Object *RootObjectPtr = ExpectedJSON->getAsObject();
+  if (!RootObjectPtr) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadObject, "WPASuite",
+                                "object")
+        .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+        .build();
+  }
+
+  const Object &RootObject = *RootObjectPtr;
+
+  WPASuite Suite = makeWPASuite();
+
+  {
+    const Array *IdTableArray = RootObject.getArray("id_table");
+    if (!IdTableArray) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedToReadObjectAtField,
+                                  "IdTable", "id_table", "array")
+          .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+          .build();
+    }
+
+    auto ExpectedIdTable = entityIdTableFromJSON(*IdTableArray);
+    if (!ExpectedIdTable) {
+      return ErrorBuilder::wrap(ExpectedIdTable.takeError())
+          .context(ErrorMessages::ReadingFromField, "IdTable", "id_table")
+          .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+          .build();
+    }
+
+    getIdTable(Suite) = std::move(*ExpectedIdTable);
+  }
+
+  {
+    const Array *ResultsArray = RootObject.getArray("results");
+    if (!ResultsArray) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedToReadObjectAtField,
+                                  "WPA results", "results", "array")
+          .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+          .build();
+    }
+
+    auto ExpectedResultsMap = analysisResultMapFromJSON(*ResultsArray);
+    if (!ExpectedResultsMap) {
+      return ErrorBuilder::wrap(ExpectedResultsMap.takeError())
+          .context(ErrorMessages::ReadingFromField, "WPA results", "results")
+          .context(ErrorMessages::ReadingFromFile, "WPASuite", Path)
+          .build();
+    }
+
+    getData(Suite) = std::move(*ExpectedResultsMap);
+  }
+
+  return Suite;
+}
+
+llvm::Error JSONFormat::writeWPASuite(const WPASuite &Suite,
+                                      llvm::StringRef Path) {
+  Object RootObject;
+
+  RootObject["id_table"] = entityIdTableToJSON(getIdTable(Suite));
+
+  auto ExpectedResults = analysisResultMapToJSON(getData(Suite));
+  if (!ExpectedResults) {
+    return ErrorBuilder::wrap(ExpectedResults.takeError())
+        .context(ErrorMessages::WritingToFile, "WPASuite", Path)
+        .build();
+  }
+
+  RootObject["results"] = std::move(*ExpectedResults);
+
+  if (auto Error = writeJSON(std::move(RootObject), Path)) {
+    return ErrorBuilder::wrap(std::move(Error))
+        .context(ErrorMessages::WritingToFile, "WPASuite", Path)
+        .build();
+  }
+
+  return llvm::Error::success();
+}
+
+} // namespace clang::ssaf
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 345eed6c5279b..f24dd837f1274 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -19,6 +19,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Serialization/JSONFormatTest/JSONFormatTest.cpp
   Serialization/JSONFormatTest/LUSummaryTest.cpp
   Serialization/JSONFormatTest/TUSummaryTest.cpp
+  Serialization/JSONFormatTest/WPASuiteTest.cpp
   SummaryData/SummaryDataTest.cpp
   SummaryNameTest.cpp
   TestFixture.cpp
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendActionTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendActionTest.cpp
index d684366ed53ce..5d2392c9236a9 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendActionTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendActionTest.cpp
@@ -99,6 +99,15 @@ class FailingSerializationFormat final : public SerializationFormat {
     return failing("writeLUSummaryEncoding");
   }
 
+  llvm::Expected<WPASuite> readWPASuite(llvm::StringRef Path) override {
+    return failing("readWPASuite");
+  }
+
+  llvm::Error writeWPASuite(const WPASuite &Suite,
+                            llvm::StringRef Path) override {
+    return failing("writeWPASuite");
+  }
+
   void forEachRegisteredAnalysis(
       llvm::function_ref<void(llvm::StringRef Name, llvm::StringRef Desc)>
           Callback) const override {}
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.cpp
index 535b5fced0da6..684249620e869 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.cpp
@@ -199,3 +199,13 @@ llvm::Error MockSerializationFormat::writeLUSummaryEncoding(
   llvm_unreachable(
       "MockSerializationFormat does not support LUSummaryEncoding");
 }
+
+llvm::Expected<WPASuite>
+MockSerializationFormat::readWPASuite(llvm::StringRef Path) {
+  llvm_unreachable("MockSerializationFormat does not support WPASuite");
+}
+
+llvm::Error MockSerializationFormat::writeWPASuite(const WPASuite &Suite,
+                                                   llvm::StringRef Path) {
+  llvm_unreachable("MockSerializationFormat does not support WPASuite");
+}
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.h b/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.h
index 87d84c3d06c7a..71acfeffca1c9 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.h
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Registries/MockSerializationFormat.h
@@ -44,6 +44,11 @@ class MockSerializationFormat final : public SerializationFormat {
   llvm::Error writeLUSummaryEncoding(const LUSummaryEncoding &SummaryEncoding,
                                      llvm::StringRef Path) override;
 
+  llvm::Expected<WPASuite> readWPASuite(llvm::StringRef Path) override;
+
+  llvm::Error writeWPASuite(const WPASuite &Suite,
+                            llvm::StringRef Path) override;
+
   /// Lists what analyses implement this particular serialisation format.
   void forEachRegisteredAnalysis(
       llvm::function_ref<void(llvm::StringRef Name, llvm::StringRef Desc)>
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
new file mode 100644
index 0000000000000..3972604a36f49
--- /dev/null
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
@@ -0,0 +1,768 @@
+//===- WPASuiteTest.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 SSAF JSON serialization format reading and writing of
+// WPASuite.
+//
+//===----------------------------------------------------------------------===//
+
+#include "JSONFormatTest.h"
+
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gmock/gmock.h"
+
+#include <memory>
+#include <vector>
+
+using namespace clang::ssaf;
+using namespace llvm;
+using ::testing::AllOf;
+using ::testing::HasSubstr;
+
+namespace {
+
+// ============================================================================
+// First Test AnalysisResult - Tags (no entity ID references)
+// ============================================================================
+
+struct TagsAnalysisResultForJSONFormatTest final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("TagsAnalysisResultForJSONFormatTest");
+  }
+
+  std::vector<std::string> Tags;
+};
+
+json::Object serializeTagsAnalysisResult(const AnalysisResult &Result,
+                                         JSONFormat::EntityIdToJSONFn) {
+  const auto &R =
+      static_cast<const TagsAnalysisResultForJSONFormatTest &>(Result);
+  json::Array TagsArray;
+  for (const auto &Tag : R.Tags)
+    TagsArray.push_back(Tag);
+  return json::Object{{"tags", std::move(TagsArray)}};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeTagsAnalysisResult(const json::Object &Obj,
+                              JSONFormat::EntityIdFromJSONFn) {
+  const json::Array *TagsArray = Obj.getArray("tags");
+  if (!TagsArray)
+    return createStringError(inconvertibleErrorCode(),
+                             "missing or invalid field 'tags'");
+
+  auto R = std::make_unique<TagsAnalysisResultForJSONFormatTest>();
+  for (const auto &[Index, Val] : llvm::enumerate(*TagsArray)) {
+    auto S = Val.getAsString();
+    if (!S)
+      return createStringError(inconvertibleErrorCode(),
+                               "tags element at index %zu is not a string",
+                               Index);
+    R->Tags.push_back(S->str());
+  }
+  return std::move(R);
+}
+
+JSONFormat::AnalysisResultRegistryGenerator::Add<
+    TagsAnalysisResultForJSONFormatTest>
+    RegisterTagsAnalysisFormatInfo(serializeTagsAnalysisResult,
+                                   deserializeTagsAnalysisResult);
+
+// ============================================================================
+// Second Test AnalysisResult - Counts (with entity ID references)
+// ============================================================================
+
+struct CountsAnalysisResultForJSONFormatTest final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("CountsAnalysisResultForJSONFormatTest");
+  }
+
+  std::vector<std::pair<EntityId, int>> Counts;
+};
+
+json::Object
+serializeCountsAnalysisResult(const AnalysisResult &Result,
+                              JSONFormat::EntityIdToJSONFn ToJSON) {
+  const auto &R =
+      static_cast<const CountsAnalysisResultForJSONFormatTest &>(Result);
+  json::Array CountsArray;
+  for (const auto &[EI, Count] : R.Counts) {
+    CountsArray.push_back(
+        json::Object{{"entity_id", ToJSON(EI)}, {"count", Count}});
+  }
+  return json::Object{{"counts", std::move(CountsArray)}};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeCountsAnalysisResult(const json::Object &Obj,
+                                JSONFormat::EntityIdFromJSONFn FromJSON) {
+  const json::Array *CountsArray = Obj.getArray("counts");
+  if (!CountsArray)
+    return createStringError(inconvertibleErrorCode(),
+                             "missing or invalid field 'counts'");
+
+  auto R = std::make_unique<CountsAnalysisResultForJSONFormatTest>();
+  for (const auto &[Index, Val] : llvm::enumerate(*CountsArray)) {
+    const json::Object *Entry = Val.getAsObject();
+    if (!Entry)
+      return createStringError(inconvertibleErrorCode(),
+                               "counts element at index %zu is not an object",
+                               Index);
+    const json::Object *EIObj = Entry->getObject("entity_id");
+    if (!EIObj)
+      return createStringError(
+          inconvertibleErrorCode(),
+          "missing or invalid 'entity_id' field at index %zu", Index);
+    auto ExpectedEI = FromJSON(*EIObj);
+    if (!ExpectedEI)
+      return ExpectedEI.takeError();
+
+    auto CountVal = Entry->getInteger("count");
+    if (!CountVal)
+      return createStringError(inconvertibleErrorCode(),
+                               "missing or invalid 'count' field at index %zu",
+                               Index);
+    R->Counts.emplace_back(*ExpectedEI, static_cast<int>(*CountVal));
+  }
+  return std::move(R);
+}
+
+JSONFormat::AnalysisResultRegistryGenerator::Add<
+    CountsAnalysisResultForJSONFormatTest>
+    RegisterCountsAnalysisFormatInfo(serializeCountsAnalysisResult,
+                                     deserializeCountsAnalysisResult);
+
+// ============================================================================
+// FailingDeserializerAnalysisResult - always returns an error on deserialize
+// ============================================================================
+
+struct FailingDeserializerAnalysisResultForJSONFormatTest final
+    : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("FailingDeserializerAnalysisResultForJSONFormatTest");
+  }
+};
+
+json::Object
+serializeFailingDeserializerAnalysisResult(const AnalysisResult &,
+                                           JSONFormat::EntityIdToJSONFn) {
+  return json::Object{};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeFailingDeserializerAnalysisResult(const json::Object &,
+                                             JSONFormat::EntityIdFromJSONFn) {
+  return createStringError(inconvertibleErrorCode(),
+                           "intentional deserializer failure");
+}
+
+JSONFormat::AnalysisResultRegistryGenerator::Add<
+    FailingDeserializerAnalysisResultForJSONFormatTest>
+    RegisterFailingDeserializerAnalysisFormatInfo(
+        serializeFailingDeserializerAnalysisResult,
+        deserializeFailingDeserializerAnalysisResult);
+
+// ============================================================================
+// JSONFormatWPASuiteTest Fixture
+// ============================================================================
+
+class JSONFormatWPASuiteTest : public JSONFormatTest {
+protected:
+  llvm::Expected<WPASuite>
+  readWPASuiteFromString(StringRef JSON,
+                         StringRef FileName = "test.json") const {
+    auto ExpectedFilePath = writeJSON(JSON, FileName);
+    if (!ExpectedFilePath)
+      return ExpectedFilePath.takeError();
+    return JSONFormat().readWPASuite(makePath(FileName));
+  }
+
+  llvm::Expected<WPASuite> readWPASuiteFromFile(StringRef FileName) const {
+    return JSONFormat().readWPASuite(makePath(FileName));
+  }
+
+  llvm::Error writeWPASuite(const WPASuite &Suite, StringRef FileName) const {
+    return JSONFormat().writeWPASuite(Suite, makePath(FileName));
+  }
+
+  // Builds an empty WPASuite (no id_table entries, no results) and writes it.
+  llvm::Error writeEmptyWPASuite(StringRef FileName) const {
+    WPASuite Suite = makeWPASuite();
+    return writeWPASuite(Suite, FileName);
+  }
+
+  void readWriteCompare(StringRef JSON) const {
+    const PathString InputFileName("input.json");
+    const PathString OutputFileName("output.json");
+
+    auto ExpectedInputFilePath = writeJSON(JSON, InputFileName);
+    ASSERT_THAT_EXPECTED(ExpectedInputFilePath, Succeeded());
+
+    auto ExpectedSuite = readWPASuiteFromFile(InputFileName);
+    ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
+
+    ASSERT_THAT_ERROR(writeWPASuite(*ExpectedSuite, OutputFileName),
+                      Succeeded());
+
+    auto ExpectedInputJSON = readJSONFromFile(InputFileName);
+    ASSERT_THAT_EXPECTED(ExpectedInputJSON, Succeeded());
+
+    auto ExpectedOutputJSON = readJSONFromFile(OutputFileName);
+    ASSERT_THAT_EXPECTED(ExpectedOutputJSON, Succeeded());
+
+    auto ExpectedNormalizedInput =
+        normalizeWPASuiteJSON(std::move(*ExpectedInputJSON));
+    ASSERT_THAT_EXPECTED(ExpectedNormalizedInput, Succeeded());
+
+    auto ExpectedNormalizedOutput =
+        normalizeWPASuiteJSON(std::move(*ExpectedOutputJSON));
+    ASSERT_THAT_EXPECTED(ExpectedNormalizedOutput, Succeeded());
+
+    ASSERT_EQ(*ExpectedNormalizedInput, *ExpectedNormalizedOutput)
+        << "Serialization is broken: input is different from output\n"
+        << "Input:  " << llvm::formatv("{0:2}", *ExpectedNormalizedInput).str()
+        << "\n"
+        << "Output: "
+        << llvm::formatv("{0:2}", *ExpectedNormalizedOutput).str();
+  }
+
+private:
+  static llvm::Expected<json::Value> normalizeWPASuiteJSON(json::Value Val) {
+    auto *Obj = Val.getAsObject();
+    if (!Obj)
+      return createStringError(
+          inconvertibleErrorCode(),
+          "Cannot normalize WPASuite JSON: expected object");
+
+    auto *IdTable = Obj->getArray("id_table");
+    if (!IdTable)
+      return createStringError(
+          inconvertibleErrorCode(),
+          "Cannot normalize WPASuite JSON: missing 'id_table' array");
+
+    llvm::sort(*IdTable, [](const json::Value &A, const json::Value &B) {
+      const auto *OA = A.getAsObject();
+      const auto *OB = B.getAsObject();
+      if (!OA || !OB)
+        return false;
+      auto IA = OA->getInteger("id");
+      auto IB = OB->getInteger("id");
+      return IA && IB && *IA < *IB;
+    });
+
+    auto *Results = Obj->getArray("results");
+    if (!Results)
+      return createStringError(
+          inconvertibleErrorCode(),
+          "Cannot normalize WPASuite JSON: missing 'results' array");
+
+    llvm::sort(*Results, [](const json::Value &A, const json::Value &B) {
+      const auto *OA = A.getAsObject();
+      const auto *OB = B.getAsObject();
+      if (!OA || !OB)
+        return false;
+      auto NA = OA->getString("analysis_name");
+      auto NB = OB->getString("analysis_name");
+      return NA && NB && *NA < *NB;
+    });
+
+    return Val;
+  }
+};
+
+// ============================================================================
+// readJSON() Error Tests
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, NonexistentFile) {
+  auto Result = readWPASuiteFromFile("nonexistent.json");
+
+  EXPECT_THAT_EXPECTED(
+      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from"),
+                                      HasSubstr("file does not exist"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, PathIsDirectory) {
+  auto ExpectedDirPath = makeDirectory("test_directory.json");
+  ASSERT_THAT_EXPECTED(ExpectedDirPath, Succeeded());
+
+  auto Result = readWPASuiteFromFile("test_directory.json");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from"),
+                              HasSubstr("path is a directory, not a file"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, NotJsonExtension) {
+  auto ExpectedFilePath = writeJSON("{}", "test.txt");
+  ASSERT_THAT_EXPECTED(ExpectedFilePath, Succeeded());
+
+  auto Result = JSONFormat().readWPASuite(makePath("test.txt"));
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read file"),
+                              HasSubstr("file does not end with '.json'"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, InvalidSyntax) {
+  auto Result = readWPASuiteFromString("{ invalid json }");
+
+  EXPECT_THAT_EXPECTED(
+      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                                      HasSubstr("Expected object key"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, NotObject) {
+  auto Result = readWPASuiteFromString("[]");
+
+  EXPECT_THAT_EXPECTED(
+      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                                      HasSubstr("failed to read WPASuite"),
+                                      HasSubstr("expected JSON object"))));
+}
+
+// ============================================================================
+// Structural Error Tests - id_table
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, MissingIdTable) {
+  auto Result = readWPASuiteFromString(R"({"results": []})");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read IdTable from field "
+                                        "'id_table'"),
+                              HasSubstr("expected JSON array"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, IdTableNotArray) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": {},
+    "results": []
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read IdTable from field "
+                                        "'id_table'"),
+                              HasSubstr("expected JSON array"))));
+}
+
+// ============================================================================
+// Structural Error Tests - results
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, MissingResults) {
+  auto Result = readWPASuiteFromString(R"({"id_table": []})");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read WPA results from field "
+                                        "'results'"),
+                              HasSubstr("expected JSON array"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultsNotArray) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": {}
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read WPA results from field "
+                                        "'results'"),
+                              HasSubstr("expected JSON array"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryNotObject) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": ["invalid"]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("reading WPA results from field "
+                                        "'results'"),
+                              HasSubstr("failed to read WPA result entry from "
+                                        "index '0'"),
+                              HasSubstr("expected JSON object"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryMissingAnalysisName) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [{"result": {}}]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("reading WPA results from field "
+                                        "'results'"),
+                              HasSubstr("reading WPA result entry from index "
+                                        "'0'"),
+                              HasSubstr("failed to read AnalysisName from "
+                                        "field 'analysis_name'"),
+                              HasSubstr("expected JSON string"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryAnalysisNameNotString) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [{"analysis_name": 42, "result": {}}]
+  })");
+
+  EXPECT_THAT_EXPECTED(Result, FailedWithMessage(AllOf(
+                                   HasSubstr("reading WPASuite from file"),
+                                   HasSubstr("failed to read AnalysisName from "
+                                             "field 'analysis_name'"),
+                                   HasSubstr("expected JSON string"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryNoFormatInfo) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {"analysis_name": "UnregisteredAnalysis", "result": {}}
+    ]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result, FailedWithMessage(
+                  AllOf(HasSubstr("reading WPASuite from file"),
+                        HasSubstr("reading WPA results from field 'results'"),
+                        HasSubstr("reading WPA result entry from index '0'"),
+                        HasSubstr("no support registered for analysis: "
+                                  "AnalysisName(UnregisteredAnalysis)"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryMissingResultField) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {"analysis_name": "TagsAnalysisResultForJSONFormatTest"}
+    ]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("reading WPA results from field "
+                                        "'results'"),
+                              HasSubstr("reading WPA result entry from index "
+                                        "'0'"),
+                              HasSubstr("failed to read AnalysisResult from "
+                                        "field 'result'"),
+                              HasSubstr("expected JSON object"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryResultNotObject) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
+       "result": "not_an_object"}
+    ]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("failed to read AnalysisResult from "
+                                        "field 'result'"),
+                              HasSubstr("expected JSON object"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, ResultEntryDeserializerError) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {"analysis_name": "FailingDeserializerAnalysisResultForJSONFormatTest",
+       "result": {}}
+    ]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
+                              HasSubstr("reading WPA results from field "
+                                        "'results'"),
+                              HasSubstr("reading WPA result entry from index "
+                                        "'0'"),
+                              HasSubstr("intentional deserializer failure"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, DuplicateAnalysisName) {
+  auto Result = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
+       "result": {"tags": []}},
+      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
+       "result": {"tags": []}}
+    ]
+  })");
+
+  EXPECT_THAT_EXPECTED(
+      Result,
+      FailedWithMessage(AllOf(
+          HasSubstr("reading WPASuite from file"),
+          HasSubstr("reading WPA results from field 'results'"),
+          HasSubstr("failed to insert WPA result at index '1'"),
+          HasSubstr("encountered duplicate "
+                    "'AnalysisName(TagsAnalysisResultForJSONFormatTest)'"))));
+}
+
+// ============================================================================
+// Write Error Tests
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, WriteFileAlreadyExists) {
+  auto ExpectedFilePath = writeJSON("{}", "existing.json");
+  ASSERT_THAT_EXPECTED(ExpectedFilePath, Succeeded());
+
+  auto Result = writeEmptyWPASuite("existing.json");
+
+  EXPECT_THAT_ERROR(
+      std::move(Result),
+      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
+                              HasSubstr("failed to write file"),
+                              HasSubstr("file already exists"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, WriteParentDirectoryNotFound) {
+  PathString FilePath = makePath("nonexistent-dir", "test.json");
+
+  auto Result = JSONFormat().writeWPASuite(makeWPASuite(), FilePath);
+
+  EXPECT_THAT_ERROR(
+      std::move(Result),
+      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
+                              HasSubstr("failed to write file"),
+                              HasSubstr("parent directory does not exist"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, WriteNotJsonExtension) {
+  auto Result =
+      JSONFormat().writeWPASuite(makeWPASuite(), makePath("test.txt"));
+
+  EXPECT_THAT_ERROR(
+      std::move(Result),
+      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
+                              HasSubstr("failed to write file"),
+                              HasSubstr("file does not end with '.json'"))));
+}
+
+TEST_F(JSONFormatWPASuiteTest, WriteNoFormatInfo) {
+  WPASuite Suite = makeWPASuite();
+  getData(Suite).emplace(
+      AnalysisName("UnregisteredAnalysisForJSONFormatTest"),
+      std::make_unique<TagsAnalysisResultForJSONFormatTest>());
+
+  auto Result = writeWPASuite(Suite, "output.json");
+
+  EXPECT_THAT_ERROR(
+      std::move(Result),
+      FailedWithMessage(AllOf(
+          HasSubstr("writing WPASuite to file"),
+          HasSubstr("no support registered for analysis: "
+                    "AnalysisName(UnregisteredAnalysisForJSONFormatTest)"))));
+}
+
+// ============================================================================
+// Round-Trip Tests
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, RoundTripEmpty) {
+  readWriteCompare(R"({
+    "id_table": [],
+    "results": []
+  })");
+}
+
+TEST_F(JSONFormatWPASuiteTest, RoundTripSingleResultNoEntities) {
+  readWriteCompare(R"({
+    "id_table": [],
+    "results": [
+      {
+        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
+        "result": {"tags": ["alpha", "beta", "gamma"]}
+      }
+    ]
+  })");
+}
+
+TEST_F(JSONFormatWPASuiteTest, RoundTripSingleResultWithEntityRefs) {
+  readWriteCompare(R"({
+    "id_table": [
+      {
+        "id": 0,
+        "name": {
+          "usr": "c:@F at foo",
+          "suffix": "",
+          "namespace": [
+            {"kind": "CompilationUnit", "name": "a.cpp"},
+            {"kind": "LinkUnit", "name": "a.exe"}
+          ]
+        }
+      },
+      {
+        "id": 1,
+        "name": {
+          "usr": "c:@F at bar",
+          "suffix": "",
+          "namespace": [
+            {"kind": "CompilationUnit", "name": "a.cpp"},
+            {"kind": "LinkUnit", "name": "a.exe"}
+          ]
+        }
+      }
+    ],
+    "results": [
+      {
+        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
+        "result": {
+          "counts": [
+            {"entity_id": {"@": 0}, "count": 42},
+            {"entity_id": {"@": 1}, "count": 7}
+          ]
+        }
+      }
+    ]
+  })");
+}
+
+TEST_F(JSONFormatWPASuiteTest, RoundTripMultipleResults) {
+  readWriteCompare(R"({
+    "id_table": [
+      {
+        "id": 0,
+        "name": {
+          "usr": "c:@F at foo",
+          "suffix": "",
+          "namespace": [
+            {"kind": "LinkUnit", "name": "test.exe"}
+          ]
+        }
+      }
+    ],
+    "results": [
+      {
+        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
+        "result": {
+          "counts": [
+            {"entity_id": {"@": 0}, "count": 100}
+          ]
+        }
+      },
+      {
+        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
+        "result": {"tags": ["important"]}
+      }
+    ]
+  })");
+}
+
+TEST_F(JSONFormatWPASuiteTest, RoundTripEmptyResultPayload) {
+  readWriteCompare(R"({
+    "id_table": [],
+    "results": [
+      {
+        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
+        "result": {"tags": []}
+      }
+    ]
+  })");
+}
+
+// ============================================================================
+// Content Verification Tests
+// ============================================================================
+
+TEST_F(JSONFormatWPASuiteTest, ReadVerifyTagsResult) {
+  auto ExpectedSuite = readWPASuiteFromString(R"({
+    "id_table": [],
+    "results": [
+      {
+        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
+        "result": {"tags": ["foo", "bar"]}
+      }
+    ]
+  })");
+
+  ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
+  const WPASuite &Suite = *ExpectedSuite;
+
+  ASSERT_TRUE(
+      Suite.contains(TagsAnalysisResultForJSONFormatTest::analysisName()));
+  auto ExpectedResult = Suite.get<TagsAnalysisResultForJSONFormatTest>();
+  ASSERT_THAT_EXPECTED(ExpectedResult, Succeeded());
+
+  const auto &R = *ExpectedResult;
+  ASSERT_EQ(R.Tags.size(), 2u);
+  EXPECT_EQ(R.Tags[0], "foo");
+  EXPECT_EQ(R.Tags[1], "bar");
+}
+
+TEST_F(JSONFormatWPASuiteTest, ReadVerifyCountsResultWithEntityId) {
+  auto ExpectedSuite = readWPASuiteFromString(R"({
+    "id_table": [
+      {
+        "id": 0,
+        "name": {
+          "usr": "c:@F at foo",
+          "suffix": "",
+          "namespace": [
+            {"kind": "CompilationUnit", "name": "test.cpp"},
+            {"kind": "LinkUnit", "name": "test.exe"}
+          ]
+        }
+      }
+    ],
+    "results": [
+      {
+        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
+        "result": {
+          "counts": [
+            {"entity_id": {"@": 0}, "count": 99}
+          ]
+        }
+      }
+    ]
+  })");
+
+  ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
+  const WPASuite &Suite = *ExpectedSuite;
+
+  ASSERT_TRUE(
+      Suite.contains(CountsAnalysisResultForJSONFormatTest::analysisName()));
+  auto ExpectedResult = Suite.get<CountsAnalysisResultForJSONFormatTest>();
+  ASSERT_THAT_EXPECTED(ExpectedResult, Succeeded());
+
+  const auto &R = *ExpectedResult;
+  ASSERT_EQ(R.Counts.size(), 1u);
+  EXPECT_EQ(R.Counts[0].second, 99);
+}
+
+} // namespace
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/TestFixture.h b/clang/unittests/ScalableStaticAnalysisFramework/TestFixture.h
index 74bc3a09dd0d8..2116c2b4a664c 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/TestFixture.h
+++ b/clang/unittests/ScalableStaticAnalysisFramework/TestFixture.h
@@ -19,6 +19,7 @@
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/SummaryName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/TUSummary/TUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
 #include "gtest/gtest.h"
 #include <iosfwd>
 
@@ -26,6 +27,8 @@ namespace clang::ssaf {
 
 class TestFixture : public ::testing::Test {
 protected:
+  static WPASuite makeWPASuite() { return WPASuite(); }
+
 #define FIELD(CLASS, FIELD_NAME)                                               \
   static const auto &get##FIELD_NAME(const CLASS &X) { return X.FIELD_NAME; }  \
   static auto &get##FIELD_NAME(CLASS &X) { return X.FIELD_NAME; }

>From 0e7637f722021731753c390e1d37dd18aa6ab22a Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Fri, 20 Mar 2026 11:51:03 -0700
Subject: [PATCH 18/30] Move testing to plugins

---
 .../CMakeLists.txt                            |   1 +
 .../plugins/CMakeLists.txt                    |  12 +
 .../ssaf-wpa-suite-test-plugin/CMakeLists.txt |  21 ++
 .../SSAFWPASuiteTestPlugin.cpp                | 225 ++++++++++++++++
 .../ssaf-analyzer/Inputs/lu-empty.json        |  11 +
 .../Analysis/Scalable/ssaf-analyzer/cli.test  |  10 +
 .../Analysis/Scalable/ssaf-analyzer/io.test   |  21 ++
 .../Scalable/ssaf-analyzer/plugin.test        |  80 ++++++
 .../Inputs/duplicate-analysis-name.json       |   7 +
 .../wpa-suite/Inputs/id-table-not-array.json  |   1 +
 .../wpa-suite/Inputs/malformed.json           |   1 +
 .../wpa-suite/Inputs/missing-id-table.json    |   1 +
 .../wpa-suite/Inputs/missing-results.json     |   1 +
 .../wpa-suite/Inputs/not-object.json          |   1 +
 ...result-entry-analysis-name-not-string.json |   1 +
 .../result-entry-deserializer-error.json      |   1 +
 .../result-entry-missing-analysis-name.json   |   1 +
 .../result-entry-missing-result-field.json    |   1 +
 .../Inputs/result-entry-no-format-info.json   |   1 +
 .../Inputs/result-entry-not-object.json       |   1 +
 .../result-entry-result-not-object.json       |   1 +
 .../wpa-suite/Inputs/results-not-array.json   |   1 +
 .../Inputs/rt-counts-with-entities.json       |  25 ++
 .../wpa-suite/Inputs/rt-empty-payload.json    |   9 +
 .../wpa-suite/Inputs/rt-empty.json            |   1 +
 .../wpa-suite/Inputs/rt-multiple-results.json |  28 ++
 .../wpa-suite/Inputs/rt-single-tags.json      |   9 +
 .../ssaf-format/wpa-suite/errors.test         | 129 +++++++++
 .../ssaf-format/wpa-suite/plugin.test         | 121 +++++++++
 clang/tools/CMakeLists.txt                    |   1 +
 .../tools/clang-ssaf-analyzer/CMakeLists.txt  |  15 ++
 .../clang-ssaf-analyzer/SSAFAnalyzer.cpp      | 253 ++++++++++++++++++
 clang/tools/clang-ssaf-format/SSAFFormat.cpp  |  10 +-
 33 files changed, 1000 insertions(+), 2 deletions(-)
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt
 create mode 100644 clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp
 create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/io.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
 create mode 100644 clang/tools/clang-ssaf-analyzer/CMakeLists.txt
 create mode 100644 clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp

diff --git a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
index 3da5c8899ddb6..a803d88cd978a 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_subdirectory(Analyses)
 add_subdirectory(Core)
 add_subdirectory(Frontend)
+add_subdirectory(plugins)
 add_subdirectory(Tool)
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt
new file mode 100644
index 0000000000000..7dba9a4b8d9a0
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt
@@ -0,0 +1,12 @@
+if(LLVM_ENABLE_PLUGINS)
+  # Plugins must never bring LLVM or Clang libraries in statically.
+  # clang-ssaf-analyzer already loads clangScalableStaticAnalysisFrameworkCore
+  # and LLVM into the process; a second static copy would produce duplicate
+  # llvm::Registry instances with separate global state, breaking registration.
+  #
+  # Instead: build each plugin as a MODULE with PLUGIN_TOOL pointing to the
+  # tool that will load it, and pull only include paths (not link deps) from
+  # the libraries whose headers are needed. All symbols are resolved from the
+  # tool's address space when the plugin is loaded via --load.
+  add_subdirectory(ssaf-wpa-suite-test-plugin)
+endif()
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt
new file mode 100644
index 0000000000000..b78297b324253
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt
@@ -0,0 +1,21 @@
+# Do NOT set LLVM_LINK_COMPONENTS here. Setting it would cause LLVM libraries
+# (e.g. Support) to be linked statically into this MODULE, producing a second
+# copy of LLVM in the process alongside the copy already loaded by
+# clang-ssaf-analyzer. Two copies means two separate llvm::Registry instances,
+# so registrations made here would not be visible to the host tool.
+#
+# The same applies to clang static libraries: do not use
+# clang_target_link_libraries here. Instead, expose only their include paths
+# via $<TARGET_PROPERTY:...,INTERFACE_INCLUDE_DIRECTORIES> and let the
+# dynamic loader resolve every SSAF and LLVM symbol from the tool's address
+# space at load time.
+
+add_llvm_library(SSAFWPASuiteTestPlugin MODULE BUILDTREE_ONLY
+  SSAFWPASuiteTestPlugin.cpp
+  PLUGIN_TOOL clang-ssaf-analyzer
+  )
+
+# Pull in SSAF and LLVM include paths without any static link dependency.
+target_include_directories(SSAFWPASuiteTestPlugin PRIVATE
+  $<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkCore,INTERFACE_INCLUDE_DIRECTORIES>
+  )
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp
new file mode 100644
index 0000000000000..465e6f1488908
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp
@@ -0,0 +1,225 @@
+//===- SSAFWPASuiteTestPlugin.cpp - WPASuite serialization test plugin ----===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+// A loadable plugin for clang-ssaf-analyzer lit tests that exercises WPASuite
+// JSON serialization. It registers two analysis result types — Tags and Counts
+// — together with their JSON serializers/deserializers and trivial analysis
+// implementations, enabling end-to-end lit tests for the WPASuite round-trip
+// without depending on any real analysis logic.
+//
+// Usage:
+//   clang-ssaf-analyzer --load <path/to/SSAFWPASuiteTestPlugin.so> \
+//     --analysis TagsAnalysisResult \
+//     --analysis CountsAnalysisResult \
+//     -o output.json input.json
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisRegistry.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/JSON.h"
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace clang::ssaf;
+using namespace llvm;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// TagsAnalysisResult
+//
+// Holds a flat list of string tags. Serialized as:
+//   { "tags": ["tag1", "tag2", ...] }
+//===----------------------------------------------------------------------===//
+
+struct TagsAnalysisResult final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("TagsAnalysisResult");
+  }
+
+  std::vector<std::string> Tags;
+};
+
+json::Object serializeTagsAnalysisResult(const AnalysisResult &Result,
+                                         JSONFormat::EntityIdToJSONFn) {
+  const auto &R = static_cast<const TagsAnalysisResult &>(Result);
+  json::Array TagsArray;
+  for (const auto &Tag : R.Tags)
+    TagsArray.push_back(Tag);
+  return json::Object{{"tags", std::move(TagsArray)}};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeTagsAnalysisResult(const json::Object &Obj,
+                              JSONFormat::EntityIdFromJSONFn) {
+  const json::Array *TagsArray = Obj.getArray("tags");
+  if (!TagsArray)
+    return createStringError(inconvertibleErrorCode(),
+                             "missing or invalid field 'tags'");
+
+  auto R = std::make_unique<TagsAnalysisResult>();
+  for (const auto &[Index, Val] : llvm::enumerate(*TagsArray)) {
+    auto S = Val.getAsString();
+    if (!S)
+      return createStringError(inconvertibleErrorCode(),
+                               "tags element at index %zu is not a string",
+                               Index);
+    R->Tags.push_back(S->str());
+  }
+  return std::move(R);
+}
+
+JSONFormat::AnalysisResultRegistry::Add<TagsAnalysisResult>
+    RegisterTagsForJSON(serializeTagsAnalysisResult,
+                        deserializeTagsAnalysisResult);
+
+//===----------------------------------------------------------------------===//
+// TagsAnalysis
+//
+// A trivial DerivedAnalysis that produces a fixed set of tags, independent
+// of the input LUSummary. Used so that lit tests have predictable output.
+//===----------------------------------------------------------------------===//
+
+class TagsAnalysis final : public DerivedAnalysis<TagsAnalysisResult> {
+public:
+  llvm::Error initialize() override { return llvm::Error::success(); }
+
+  llvm::Expected<bool> step() override {
+    result().Tags = {"alpha", "beta", "gamma"};
+    return false; // converged after one step
+  }
+};
+
+AnalysisRegistry::Add<TagsAnalysis> RegisterTagsAnalysis(
+    "Produces a fixed list of string tags for testing WPASuite serialization");
+
+//===----------------------------------------------------------------------===//
+// CountsAnalysisResult
+//
+// Holds a list of (EntityId, int) count pairs. Serialized as:
+//   { "counts": [{"entity_id": {...}, "count": N}, ...] }
+//===----------------------------------------------------------------------===//
+
+struct CountsAnalysisResult final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("CountsAnalysisResult");
+  }
+
+  std::vector<std::pair<EntityId, int>> Counts;
+};
+
+json::Object
+serializeCountsAnalysisResult(const AnalysisResult &Result,
+                              JSONFormat::EntityIdToJSONFn ToJSON) {
+  const auto &R = static_cast<const CountsAnalysisResult &>(Result);
+  json::Array CountsArray;
+  for (const auto &[EI, Count] : R.Counts) {
+    CountsArray.push_back(
+        json::Object{{"entity_id", ToJSON(EI)}, {"count", Count}});
+  }
+  return json::Object{{"counts", std::move(CountsArray)}};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeCountsAnalysisResult(const json::Object &Obj,
+                                JSONFormat::EntityIdFromJSONFn FromJSON) {
+  const json::Array *CountsArray = Obj.getArray("counts");
+  if (!CountsArray)
+    return createStringError(inconvertibleErrorCode(),
+                             "missing or invalid field 'counts'");
+
+  auto R = std::make_unique<CountsAnalysisResult>();
+  for (const auto &[Index, Val] : llvm::enumerate(*CountsArray)) {
+    const json::Object *Entry = Val.getAsObject();
+    if (!Entry)
+      return createStringError(inconvertibleErrorCode(),
+                               "counts element at index %zu is not an object",
+                               Index);
+    const json::Object *EIObj = Entry->getObject("entity_id");
+    if (!EIObj)
+      return createStringError(
+          inconvertibleErrorCode(),
+          "missing or invalid 'entity_id' field at index %zu", Index);
+    auto ExpectedEI = FromJSON(*EIObj);
+    if (!ExpectedEI)
+      return ExpectedEI.takeError();
+
+    auto CountVal = Entry->getInteger("count");
+    if (!CountVal)
+      return createStringError(inconvertibleErrorCode(),
+                               "missing or invalid 'count' field at index %zu",
+                               Index);
+    R->Counts.emplace_back(*ExpectedEI, static_cast<int>(*CountVal));
+  }
+  return std::move(R);
+}
+
+JSONFormat::AnalysisResultRegistry::Add<CountsAnalysisResult>
+    RegisterCountsForJSON(serializeCountsAnalysisResult,
+                          deserializeCountsAnalysisResult);
+
+//===----------------------------------------------------------------------===//
+// CountsAnalysis
+//
+// A trivial DerivedAnalysis that produces an empty counts result. Entity ID
+// serialization is exercised by round-trip tests that supply pre-built JSON.
+//===----------------------------------------------------------------------===//
+
+class CountsAnalysis final : public DerivedAnalysis<CountsAnalysisResult> {
+public:
+  llvm::Error initialize() override { return llvm::Error::success(); }
+
+  llvm::Expected<bool> step() override {
+    // Produces no counts; entity-ID round-trip is covered by direct JSON tests.
+    return false;
+  }
+};
+
+AnalysisRegistry::Add<CountsAnalysis> RegisterCountsAnalysis(
+    "Produces an empty counts result for testing WPASuite serialization");
+
+//===----------------------------------------------------------------------===//
+// FailingDeserializerAnalysisResult
+//
+// An analysis result whose deserializer always returns an error. Used to
+// exercise error propagation in the WPASuite read path without needing a
+// malformed payload.
+//===----------------------------------------------------------------------===//
+
+struct FailingDeserializerAnalysisResult final : AnalysisResult {
+  static AnalysisName analysisName() {
+    return AnalysisName("FailingDeserializerAnalysisResult");
+  }
+};
+
+json::Object
+serializeFailingDeserializerAnalysisResult(const AnalysisResult &,
+                                           JSONFormat::EntityIdToJSONFn) {
+  return json::Object{};
+}
+
+Expected<std::unique_ptr<AnalysisResult>>
+deserializeFailingDeserializerAnalysisResult(const json::Object &,
+                                             JSONFormat::EntityIdFromJSONFn) {
+  return createStringError(inconvertibleErrorCode(),
+                           "intentional deserializer failure");
+}
+
+JSONFormat::AnalysisResultRegistry::Add<FailingDeserializerAnalysisResult>
+    RegisterFailingDeserializerForJSON(
+        serializeFailingDeserializerAnalysisResult,
+        deserializeFailingDeserializerAnalysisResult);
+
+} // namespace
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
new file mode 100644
index 0000000000000..2255b4eb1ae38
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
@@ -0,0 +1,11 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "test"
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test b/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
new file mode 100644
index 0000000000000..4f1b24f34247c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
@@ -0,0 +1,10 @@
+// Tests for clang-ssaf-analyzer command-line option validation.
+
+// RUN: not clang-ssaf-analyzer 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-ARGS
+// NO-ARGS:      clang-ssaf-analyzer{{(\.exe)?}}: error: no LUSummary file specified
+// NO-ARGS-NOT:  {{.+}}
+
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-empty.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT
+// NO-OUTPUT: clang-ssaf-analyzer{{(\.exe)?}}: for the -o option: must be specified at least once!
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/io.test b/clang/test/Analysis/Scalable/ssaf-analyzer/io.test
new file mode 100644
index 0000000000000..e5a7d7a06bd91
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/io.test
@@ -0,0 +1,21 @@
+// Tests for clang-ssaf-analyzer file I/O error handling.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// Nonexistent input file.
+// RUN: not clang-ssaf-analyzer %t/nonexistent.json -o %t/out.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT
+// NO-INPUT: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}nonexistent.json'{{.*}}
+
+// Input path with no extension.
+// RUN: cp %S/Inputs/lu-empty.json %t/lu-noext
+// RUN: not clang-ssaf-analyzer %t/lu-noext -o %t/out.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-EXT
+// NO-EXT: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}lu-noext'{{.*}}
+
+// Output parent directory does not exist.
+// RUN: not clang-ssaf-analyzer %S/Inputs/lu-empty.json \
+// RUN:   -o %t/nosuchdir/out.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT-DIR
+// NO-OUTPUT-DIR: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.json'{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test b/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
new file mode 100644
index 0000000000000..b56446f510190
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
@@ -0,0 +1,80 @@
+// Tests for clang-ssaf-analyzer WPASuite serialization via the test plugin.
+// The SSAFWPASuiteTestPlugin registers TagsAnalysisResult and
+// CountsAnalysisResult together with their JSON serializers so that the
+// round-trip through clang-ssaf-analyzer can be verified end-to-end.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Run TagsAnalysis only -- verify the output JSON structure.
+// ============================================================================
+
+// RUN: clang-ssaf-analyzer \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --analysis TagsAnalysisResult \
+// RUN:   -o %t/tags.json \
+// RUN:   %S/Inputs/lu-empty.json
+// RUN: FileCheck %s --input-file %t/tags.json --check-prefix=TAGS-OUT
+
+// TAGS-OUT:      "analysis_name": "TagsAnalysisResult"
+// TAGS-OUT:      "tags":
+// TAGS-OUT-DAG:  "alpha"
+// TAGS-OUT-DAG:  "beta"
+// TAGS-OUT-DAG:  "gamma"
+
+// ============================================================================
+// Run CountsAnalysis only -- verify the output JSON structure.
+// ============================================================================
+
+// RUN: clang-ssaf-analyzer \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --analysis CountsAnalysisResult \
+// RUN:   -o %t/counts.json \
+// RUN:   %S/Inputs/lu-empty.json
+// RUN: FileCheck %s --input-file %t/counts.json --check-prefix=COUNTS-OUT
+
+// COUNTS-OUT: "analysis_name": "CountsAnalysisResult"
+// COUNTS-OUT: "counts": []
+
+// ============================================================================
+// Run both analyses -- verify both results appear in the output.
+// ============================================================================
+
+// RUN: clang-ssaf-analyzer \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --analysis TagsAnalysisResult \
+// RUN:   --analysis CountsAnalysisResult \
+// RUN:   -o %t/both.json \
+// RUN:   %S/Inputs/lu-empty.json
+// RUN: FileCheck %s --input-file %t/both.json --check-prefix=BOTH-OUT
+
+// BOTH-OUT-DAG: "analysis_name": "TagsAnalysisResult"
+// BOTH-OUT-DAG: "analysis_name": "CountsAnalysisResult"
+
+// ============================================================================
+// Run all registered analyses (no --analysis flags).
+// ============================================================================
+
+// RUN: clang-ssaf-analyzer \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   -o %t/all.json \
+// RUN:   %S/Inputs/lu-empty.json
+// RUN: FileCheck %s --input-file %t/all.json --check-prefix=ALL-OUT
+
+// ALL-OUT-DAG: "analysis_name": "TagsAnalysisResult"
+// ALL-OUT-DAG: "analysis_name": "CountsAnalysisResult"
+
+// ============================================================================
+// Unknown analysis name produces an error.
+// ============================================================================
+
+// RUN: not clang-ssaf-analyzer \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --analysis UnknownAnalysis \
+// RUN:   -o %t/unknown.json \
+// RUN:   %S/Inputs/lu-empty.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=UNKNOWN-ANALYSIS
+// UNKNOWN-ANALYSIS: error: {{.*}}UnknownAnalysis{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json
new file mode 100644
index 0000000000000..975dbfb7c6b41
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json
@@ -0,0 +1,7 @@
+{
+  "id_table": [],
+  "results": [
+    {"analysis_name": "TagsAnalysisResult", "result": {"tags": []}},
+    {"analysis_name": "TagsAnalysisResult", "result": {"tags": []}}
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
new file mode 100644
index 0000000000000..8754744da6aae
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
@@ -0,0 +1 @@
+{"id_table": {}, "results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
new file mode 100644
index 0000000000000..b0e13f61aa06e
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
@@ -0,0 +1 @@
+{ invalid json }
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
new file mode 100644
index 0000000000000..330e8fe3131fd
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
@@ -0,0 +1 @@
+{"results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json
new file mode 100644
index 0000000000000..ad430aa5b4d53
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json
@@ -0,0 +1 @@
+{"id_table": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
new file mode 100644
index 0000000000000..fe51488c7066f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
@@ -0,0 +1 @@
+[]
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json
new file mode 100644
index 0000000000000..e9208de987f1b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"analysis_name": 42, "result": {}}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json
new file mode 100644
index 0000000000000..7f1200e06201f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"analysis_name": "FailingDeserializerAnalysisResult", "result": {}}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json
new file mode 100644
index 0000000000000..35452aec3604d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"result": {}}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json
new file mode 100644
index 0000000000000..f989e9a22cff2
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"analysis_name": "TagsAnalysisResult"}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json
new file mode 100644
index 0000000000000..7dec19ffb2d4a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"analysis_name": "UnregisteredAnalysis", "result": {}}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json
new file mode 100644
index 0000000000000..5fc110b069dd8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json
@@ -0,0 +1 @@
+{"id_table": [], "results": ["invalid"]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json
new file mode 100644
index 0000000000000..3ee24d8d09065
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json
@@ -0,0 +1 @@
+{"id_table": [], "results": [{"analysis_name": "TagsAnalysisResult", "result": "not_an_object"}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json
new file mode 100644
index 0000000000000..52e44a5fb7c2e
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json
@@ -0,0 +1 @@
+{"id_table": [], "results": {}}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
new file mode 100644
index 0000000000000..f5bc930231606
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
@@ -0,0 +1,25 @@
+{
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "usr": "c:@F at foo",
+        "suffix": "",
+        "namespace": [
+          {"kind": "CompilationUnit", "name": "a.cpp"},
+          {"kind": "LinkUnit", "name": "a.exe"}
+        ]
+      }
+    }
+  ],
+  "results": [
+    {
+      "analysis_name": "CountsAnalysisResult",
+      "result": {
+        "counts": [
+          {"entity_id": {"@": 0}, "count": 42}
+        ]
+      }
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json
new file mode 100644
index 0000000000000..590b413e530c3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json
@@ -0,0 +1,9 @@
+{
+  "id_table": [],
+  "results": [
+    {
+      "analysis_name": "TagsAnalysisResult",
+      "result": {"tags": []}
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
new file mode 100644
index 0000000000000..761ca67c45e9f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
@@ -0,0 +1 @@
+{"id_table": [], "results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json
new file mode 100644
index 0000000000000..5098e0eaa787a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json
@@ -0,0 +1,28 @@
+{
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "usr": "c:@F at foo",
+        "suffix": "",
+        "namespace": [
+          {"kind": "LinkUnit", "name": "test.exe"}
+        ]
+      }
+    }
+  ],
+  "results": [
+    {
+      "analysis_name": "CountsAnalysisResult",
+      "result": {
+        "counts": [
+          {"entity_id": {"@": 0}, "count": 100}
+        ]
+      }
+    },
+    {
+      "analysis_name": "TagsAnalysisResult",
+      "result": {"tags": ["important"]}
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
new file mode 100644
index 0000000000000..fcd010f124407
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
@@ -0,0 +1,9 @@
+{
+  "id_table": [],
+  "results": [
+    {
+      "analysis_name": "TagsAnalysisResult",
+      "result": {"tags": ["alpha", "beta", "gamma"]}
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
new file mode 100644
index 0000000000000..81b2733e5b882
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
@@ -0,0 +1,129 @@
+// Tests for clang-ssaf-format --type wpa error handling.
+// None of these tests require a plugin.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Read errors: file system
+// ============================================================================
+
+// Nonexistent file.
+// RUN: not clang-ssaf-format --type wpa %t/nonexistent.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-FILE
+// NO-FILE: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}nonexistent.json'{{.*}}
+
+// Input path is a directory.
+// RUN: mkdir %t/test_directory.json
+// RUN: not clang-ssaf-format --type wpa %t/test_directory.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=IS-DIR
+// IS-DIR: clang-ssaf-format{{(\.exe)?}}: error: {{.*}}test_directory.json{{.*}}
+
+// Input has an unrecognised extension (no format registered for '.txt').
+// RUN: cp %S/Inputs/rt-empty.json %t/test.txt
+// RUN: not clang-ssaf-format --type wpa %t/test.txt 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-EXT
+// BAD-EXT: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}test.txt'{{.*}}
+
+// ============================================================================
+// Read errors: JSON structure
+// ============================================================================
+
+// Invalid JSON syntax.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/malformed.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-JSON
+// BAD-JSON:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}malformed.json'
+// BAD-JSON-NEXT: {{.*}}Expected object key{{.*}}
+
+// Top-level value is not a JSON object.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/not-object.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NOT-OBJ
+// NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}not-object.json'
+// NOT-OBJ-NEXT: failed to read WPASuite: expected JSON object
+
+// ============================================================================
+// Read errors: id_table field
+// ============================================================================
+
+// 'id_table' field is absent.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/missing-id-table.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-ID-TABLE
+// NO-ID-TABLE:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}missing-id-table.json'
+// NO-ID-TABLE-NEXT: failed to read IdTable from field 'id_table': expected JSON array
+
+// 'id_table' is an object, not an array.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/id-table-not-array.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ID-TABLE-OBJ
+// ID-TABLE-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}id-table-not-array.json'
+// ID-TABLE-OBJ-NEXT: failed to read IdTable from field 'id_table': expected JSON array
+
+// ============================================================================
+// Read errors: results field
+// ============================================================================
+
+// 'results' field is absent.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/missing-results.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-RESULTS
+// NO-RESULTS:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}missing-results.json'
+// NO-RESULTS-NEXT: failed to read WPA results from field 'results': expected JSON array
+
+// 'results' is an object, not an array.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/results-not-array.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=RESULTS-OBJ
+// RESULTS-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}results-not-array.json'
+// RESULTS-OBJ-NEXT: failed to read WPA results from field 'results': expected JSON array
+
+// A result entry is a string, not an object.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/result-entry-not-object.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ENTRY-NOT-OBJ
+// ENTRY-NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}result-entry-not-object.json'
+// ENTRY-NOT-OBJ-NEXT: reading WPA results from field 'results'
+// ENTRY-NOT-OBJ-NEXT: failed to read WPA result entry from index '0': expected JSON object
+
+// 'analysis_name' field is absent.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-missing-analysis-name.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-NAME
+// NO-NAME:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// NO-NAME:      reading WPA result entry from index '0'
+// NO-NAME-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
+
+// 'analysis_name' is an integer, not a string.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-analysis-name-not-string.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NAME-NOT-STR
+// NAME-NOT-STR:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// NAME-NOT-STR-NEXT: reading WPA results from field 'results'
+// NAME-NOT-STR-NEXT: reading WPA result entry from index '0'
+// NAME-NOT-STR-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
+
+// Analysis name is not registered (no plugin loaded).
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-no-format-info.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-FORMAT-INFO
+// NO-FORMAT-INFO:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// NO-FORMAT-INFO:      reading WPA result entry from index '0'
+// NO-FORMAT-INFO-NEXT: no support registered for analysis: AnalysisName(UnregisteredAnalysis)
+
+// ============================================================================
+// Write errors
+// ============================================================================
+
+// Output file already exists.
+// RUN: touch %t/existing.json
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/existing.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUT-EXISTS
+// OUT-EXISTS: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}existing.json'{{.*}}Output file already exists
+
+// Output parent directory does not exist.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/nosuchdir/out.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-OUT-DIR
+// NO-OUT-DIR: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.json'{{.*}}
+
+// Output path has an unrecognised extension.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/out.txt 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-OUT-EXT
+// BAD-OUT-EXT: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.txt'{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
new file mode 100644
index 0000000000000..45258aaf6f6d0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
@@ -0,0 +1,121 @@
+// Tests for clang-ssaf-format --type wpa that require the WPASuite test
+// plugin. Covers:
+//   - Read errors that need a registered deserializer to reach
+//   - Round-trip correctness (read → write → verify)
+//   - Content verification (field values in the output)
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Read errors requiring a registered deserializer
+// ============================================================================
+
+// 'result' field is absent; the deserializer lookup succeeds but the field
+// check fires before the deserializer is called.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/result-entry-missing-result-field.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-RESULT-FIELD
+// NO-RESULT-FIELD:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// NO-RESULT-FIELD:      reading WPA result entry from index '0'
+// NO-RESULT-FIELD-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
+
+// 'result' is a string, not an object.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/result-entry-result-not-object.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=RESULT-NOT-OBJ
+// RESULT-NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// RESULT-NOT-OBJ-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
+
+// Deserializer returns an error.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/result-entry-deserializer-error.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DESER-ERR
+// DESER-ERR:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// DESER-ERR:      reading WPA result entry from index '0'
+// DESER-ERR-NEXT: intentional deserializer failure
+
+// Duplicate analysis name in results array.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/duplicate-analysis-name.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUPLICATE
+// DUPLICATE:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
+// DUPLICATE:      reading WPA results from field 'results'
+// DUPLICATE-NEXT: failed to insert WPA result at index '1': encountered duplicate 'AnalysisName(TagsAnalysisResult)'
+
+// ============================================================================
+// Round-trip: RoundTripEmpty
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/rt-empty.json \
+// RUN:   -o %t/rt-empty.json
+// RUN: FileCheck %s --input-file %t/rt-empty.json --check-prefix=RT-EMPTY
+// RT-EMPTY: "id_table": []
+// RT-EMPTY: "results": []
+
+// ============================================================================
+// Round-trip + content verification: RoundTripSingleResultNoEntities /
+//                                    ReadVerifyTagsResult
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/rt-single-tags.json \
+// RUN:   -o %t/rt-single-tags.json
+// RUN: FileCheck %s --input-file %t/rt-single-tags.json --check-prefix=RT-TAGS
+// RT-TAGS:     "analysis_name": "TagsAnalysisResult"
+// RT-TAGS:     "tags":
+// RT-TAGS-DAG: "alpha"
+// RT-TAGS-DAG: "beta"
+// RT-TAGS-DAG: "gamma"
+
+// ============================================================================
+// Round-trip + content verification: RoundTripEmptyResultPayload
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/rt-empty-payload.json \
+// RUN:   -o %t/rt-empty-payload.json
+// RUN: FileCheck %s --input-file %t/rt-empty-payload.json --check-prefix=RT-EMPTY-PAYLOAD
+// RT-EMPTY-PAYLOAD: "analysis_name": "TagsAnalysisResult"
+// RT-EMPTY-PAYLOAD: "tags": []
+
+// ============================================================================
+// Round-trip + content verification: RoundTripSingleResultWithEntityRefs /
+//                                    ReadVerifyCountsResultWithEntityId
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/rt-counts-with-entities.json \
+// RUN:   -o %t/rt-counts-with-entities.json
+// RUN: FileCheck %s --input-file %t/rt-counts-with-entities.json --check-prefix=RT-COUNTS
+// RT-COUNTS:     "analysis_name": "CountsAnalysisResult"
+// RT-COUNTS:     "counts":
+// RT-COUNTS:     "entity_id":
+// RT-COUNTS:     "@":
+// RT-COUNTS:     "count": 42
+// The entity appears in the id_table with its original name.
+// RT-COUNTS:     "usr": "c:@F at foo"
+
+// ============================================================================
+// Round-trip: RoundTripMultipleResults
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   %S/Inputs/rt-multiple-results.json \
+// RUN:   -o %t/rt-multiple-results.json
+// RUN: FileCheck %s --input-file %t/rt-multiple-results.json --check-prefix=RT-MULTI
+// RT-MULTI-DAG: "analysis_name": "CountsAnalysisResult"
+// RT-MULTI-DAG: "analysis_name": "TagsAnalysisResult"
+// RT-MULTI:     "important"
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index 891043ec31f77..a16089be041bf 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -15,6 +15,7 @@ add_clang_subdirectory(clang-linker-wrapper)
 add_clang_subdirectory(clang-nvlink-wrapper)
 add_clang_subdirectory(clang-offload-bundler)
 add_clang_subdirectory(clang-scan-deps)
+add_clang_subdirectory(clang-ssaf-analyzer)
 add_clang_subdirectory(clang-ssaf-format)
 add_clang_subdirectory(clang-ssaf-linker)
 add_clang_subdirectory(clang-sycl-linker)
diff --git a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
new file mode 100644
index 0000000000000..022032beb262d
--- /dev/null
+++ b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
@@ -0,0 +1,15 @@
+set(LLVM_LINK_COMPONENTS
+  Option
+  Support
+  )
+
+add_clang_tool(clang-ssaf-analyzer
+  SSAFAnalyzer.cpp
+  )
+
+clang_target_link_libraries(clang-ssaf-analyzer
+  PRIVATE
+  clangBasic
+  clangScalableStaticAnalysisFrameworkCore
+  clangScalableStaticAnalysisFrameworkTool
+  )
diff --git a/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp b/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
new file mode 100644
index 0000000000000..d4f0c5a8c4145
--- /dev/null
+++ b/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
@@ -0,0 +1,253 @@
+//===- SSAFAnalyzer.cpp - SSAF Analyzer Tool ------------------------------===//
+//
+// 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 implements the SSAF analyzer tool that runs whole-program analyses
+//  over an LUSummary and writes the resulting WPASuite.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysisFramework/SSAFForceLinker.h" // IWYU pragma: keep
+#include "clang/ScalableStaticAnalysisFramework/Tool/Utils.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/Timer.h"
+#include "llvm/Support/WithColor.h"
+#include "llvm/Support/raw_ostream.h"
+#include <memory>
+#include <string>
+#include <system_error>
+#include <vector>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace fs = llvm::sys::fs;
+namespace path = llvm::sys::path;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Command-Line Options
+//===----------------------------------------------------------------------===//
+
+cl::OptionCategory SsafAnalyzerCategory("clang-ssaf-analyzer options");
+
+cl::list<std::string> LoadPlugins("load",
+                                  cl::desc("Load a plugin shared library"),
+                                  cl::value_desc("path"),
+                                  cl::cat(SsafAnalyzerCategory));
+
+cl::opt<std::string> LUSummaryPath(cl::Positional, cl::desc("<lu-summary>"),
+                                   cl::cat(SsafAnalyzerCategory));
+
+cl::opt<std::string> OutputPath("o", cl::desc("Output WPASuite path"),
+                                cl::value_desc("path"), cl::Required,
+                                cl::cat(SsafAnalyzerCategory));
+
+cl::list<std::string>
+    AnalysisNameStrs("analysis",
+                     cl::desc("Name of an analysis to run (may be repeated; "
+                              "default: run all registered analyses)"),
+                     cl::value_desc("name"), cl::cat(SsafAnalyzerCategory));
+
+cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
+                      cl::init(false), cl::cat(SsafAnalyzerCategory));
+
+cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
+                   cl::cat(SsafAnalyzerCategory));
+
+//===----------------------------------------------------------------------===//
+// Error Messages
+//===----------------------------------------------------------------------===//
+
+namespace LocalErrorMessages {
+
+constexpr const char *RunningAnalysis = "Running analysis '{0}'";
+
+} // namespace LocalErrorMessages
+
+//===----------------------------------------------------------------------===//
+// Diagnostic Utilities
+//===----------------------------------------------------------------------===//
+
+constexpr unsigned IndentationWidth = 2;
+
+template <typename... Ts>
+void info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
+  if (Verbose) {
+    llvm::WithColor::note()
+        << std::string(IndentationLevel * IndentationWidth, ' ') << "- "
+        << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
+  }
+}
+
+//===----------------------------------------------------------------------===//
+// Data Structures
+//===----------------------------------------------------------------------===//
+
+struct AnalyzerInput {
+  SummaryFile LUSummaryFile;
+  SummaryFile WPAOutputFile;
+  std::vector<AnalysisName> Names; // Empty means run all registered analyses.
+};
+
+//===----------------------------------------------------------------------===//
+// Pipeline
+//===----------------------------------------------------------------------===//
+
+AnalyzerInput validate(llvm::TimerGroup &TG) {
+  llvm::Timer TValidate("validate", "Validate Input", TG);
+  llvm::TimeRegion _(Time ? &TValidate : nullptr);
+
+  AnalyzerInput AI;
+
+  // Validate the LUSummary input path.
+  {
+    if (LUSummaryPath.empty()) {
+      fail("no LUSummary file specified");
+    }
+
+    llvm::SmallString<256> RealInputPath;
+    if (std::error_code EC =
+            fs::real_path(LUSummaryPath, RealInputPath, /*expand_tilde=*/true))
+      fail(ErrorMessages::CannotValidateSummary, LUSummaryPath, EC.message());
+
+    AI.LUSummaryFile = SummaryFile::fromPath(RealInputPath);
+  }
+
+  info(2, "Validated LUSummary input path '{0}'.", AI.LUSummaryFile.Path);
+
+  // Validate the WPASuite output path.
+  {
+    llvm::StringRef ParentDir = path::parent_path(OutputPath);
+    llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
+
+    if (!fs::exists(DirToCheck)) {
+      fail(ErrorMessages::CannotValidateSummary, OutputPath,
+           ErrorMessages::OutputDirectoryMissing);
+    }
+
+    // The output file does not exist yet, so real_path cannot be called on it
+    // directly. Resolve the parent directory first, then append the filename.
+    llvm::SmallString<256> RealParentDir;
+    if (std::error_code EC = fs::real_path(DirToCheck, RealParentDir))
+      fail(ErrorMessages::CannotValidateSummary, OutputPath, EC.message());
+
+    llvm::SmallString<256> RealOutputPath = RealParentDir;
+    path::append(RealOutputPath, path::filename(OutputPath));
+
+    AI.WPAOutputFile = SummaryFile::fromPath(RealOutputPath);
+  }
+
+  info(2, "Validated WPASuite output path '{0}'.", AI.WPAOutputFile.Path);
+
+  // Convert analysis name strings to AnalysisName objects.
+  for (const auto &Name : AnalysisNameStrs)
+    AI.Names.emplace_back(Name);
+
+  if (AI.Names.empty())
+    info(2, "No analyses specified; all registered analyses will be run.");
+  else
+    info(2, "Running {0} named {1}.", AI.Names.size(),
+         AI.Names.size() == 1 ? "analysis" : "analyses");
+
+  return AI;
+}
+
+void analyze(const AnalyzerInput &AI, llvm::TimerGroup &TG) {
+  llvm::Timer TRead("read", "Read LUSummary", TG);
+  llvm::Timer TRun("run", "Run Analyses", TG);
+  llvm::Timer TWrite("write", "Write WPASuite", TG);
+
+  // Read the LUSummary.
+  std::unique_ptr<LUSummary> LU;
+  {
+    info(2, "Reading LUSummary from '{0}'.", AI.LUSummaryFile.Path);
+    llvm::TimeRegion _(Time ? &TRead : nullptr);
+
+    auto ExpectedLU =
+        AI.LUSummaryFile.Format->readLUSummary(AI.LUSummaryFile.Path);
+    if (!ExpectedLU)
+      fail(ExpectedLU.takeError());
+
+    LU = std::make_unique<LUSummary>(std::move(*ExpectedLU));
+  }
+
+  // Run analyses.
+  WPASuite Suite;
+  {
+    info(2, "Running analyses.");
+    llvm::TimeRegion _(Time ? &TRun : nullptr);
+
+    AnalysisDriver Driver(std::move(LU));
+
+    llvm::Expected<WPASuite> ExpectedSuite =
+        AI.Names.empty() ? std::move(Driver).run() : Driver.run(AI.Names);
+    if (!ExpectedSuite)
+      fail(ExpectedSuite.takeError());
+
+    Suite = std::move(*ExpectedSuite);
+  }
+
+  // Write the WPASuite.
+  {
+    info(2, "Writing WPASuite to '{0}'.", AI.WPAOutputFile.Path);
+    llvm::TimeRegion _(Time ? &TWrite : nullptr);
+
+    if (auto Err = AI.WPAOutputFile.Format->writeWPASuite(
+            Suite, AI.WPAOutputFile.Path))
+      fail(std::move(Err));
+  }
+}
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Driver
+//===----------------------------------------------------------------------===//
+
+int main(int argc, const char **argv) {
+  llvm::StringRef ToolHeading = "SSAF Analyzer";
+
+  InitLLVM X(argc, argv);
+  initTool(argc, argv, "0.1", SsafAnalyzerCategory, ToolHeading);
+
+  loadPlugins(LoadPlugins);
+
+  llvm::TimerGroup AnalyzerTimers(getToolName(), ToolHeading);
+
+  {
+    info(0, "Analysis started.");
+
+    AnalyzerInput AI;
+
+    {
+      info(1, "Validating input.");
+      AI = validate(AnalyzerTimers);
+    }
+
+    {
+      info(1, "Running analyses.");
+      analyze(AI, AnalyzerTimers);
+    }
+
+    info(0, "Analysis finished.");
+  }
+
+  return 0;
+}
diff --git a/clang/tools/clang-ssaf-format/SSAFFormat.cpp b/clang/tools/clang-ssaf-format/SSAFFormat.cpp
index 4f337bff89ee1..16e87a67969ff 100644
--- a/clang/tools/clang-ssaf-format/SSAFFormat.cpp
+++ b/clang/tools/clang-ssaf-format/SSAFFormat.cpp
@@ -44,7 +44,7 @@ namespace path = llvm::sys::path;
 // Summary Type
 //===----------------------------------------------------------------------===//
 
-enum class SummaryType { TU, LU };
+enum class SummaryType { TU, LU, WPA };
 
 //===----------------------------------------------------------------------===//
 // Command-Line Options
@@ -62,7 +62,9 @@ cl::list<std::string> LoadPlugins("load",
 cl::opt<SummaryType> Type(
     "type", cl::desc("Summary type (required unless --list is given)"),
     cl::values(clEnumValN(SummaryType::TU, "tu", "Translation unit summary"),
-               clEnumValN(SummaryType::LU, "lu", "Link unit summary")),
+               clEnumValN(SummaryType::LU, "lu", "Link unit summary"),
+               clEnumValN(SummaryType::WPA, "wpa",
+                          "Whole-program analysis suite")),
     cl::cat(SsafFormatCategory));
 
 cl::opt<std::string> InputPath(cl::Positional, cl::desc("<input file>"),
@@ -338,6 +340,10 @@ void convert(const FormatInput &FI) {
           &SerializationFormat::writeLUSummary);
     }
     return;
+  case SummaryType::WPA:
+    run(FI, &SerializationFormat::readWPASuite,
+        &SerializationFormat::writeWPASuite);
+    return;
   }
 
   llvm_unreachable("Unhandled SummaryType variant");

>From e9dd7b65806b13742f1d8c7232ce098c0ce7d87f Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 11:53:32 -0700
Subject: [PATCH 19/30] Remove analyzer from this PR

---
 clang/tools/CMakeLists.txt                    |   1 -
 .../tools/clang-ssaf-analyzer/CMakeLists.txt  |  15 --
 .../clang-ssaf-analyzer/SSAFAnalyzer.cpp      | 253 ------------------
 3 files changed, 269 deletions(-)
 delete mode 100644 clang/tools/clang-ssaf-analyzer/CMakeLists.txt
 delete mode 100644 clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp

diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index a16089be041bf..891043ec31f77 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -15,7 +15,6 @@ add_clang_subdirectory(clang-linker-wrapper)
 add_clang_subdirectory(clang-nvlink-wrapper)
 add_clang_subdirectory(clang-offload-bundler)
 add_clang_subdirectory(clang-scan-deps)
-add_clang_subdirectory(clang-ssaf-analyzer)
 add_clang_subdirectory(clang-ssaf-format)
 add_clang_subdirectory(clang-ssaf-linker)
 add_clang_subdirectory(clang-sycl-linker)
diff --git a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
deleted file mode 100644
index 022032beb262d..0000000000000
--- a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
+++ /dev/null
@@ -1,15 +0,0 @@
-set(LLVM_LINK_COMPONENTS
-  Option
-  Support
-  )
-
-add_clang_tool(clang-ssaf-analyzer
-  SSAFAnalyzer.cpp
-  )
-
-clang_target_link_libraries(clang-ssaf-analyzer
-  PRIVATE
-  clangBasic
-  clangScalableStaticAnalysisFrameworkCore
-  clangScalableStaticAnalysisFrameworkTool
-  )
diff --git a/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp b/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
deleted file mode 100644
index d4f0c5a8c4145..0000000000000
--- a/clang/tools/clang-ssaf-analyzer/SSAFAnalyzer.cpp
+++ /dev/null
@@ -1,253 +0,0 @@
-//===- SSAFAnalyzer.cpp - SSAF Analyzer Tool ------------------------------===//
-//
-// 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 implements the SSAF analyzer tool that runs whole-program analyses
-//  over an LUSummary and writes the resulting WPASuite.
-//
-//===----------------------------------------------------------------------===//
-
-#include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/LUSummary.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisDriver.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
-#include "clang/ScalableStaticAnalysisFramework/SSAFForceLinker.h" // IWYU pragma: keep
-#include "clang/ScalableStaticAnalysisFramework/Tool/Utils.h"
-#include "llvm/ADT/STLExtras.h"
-#include "llvm/ADT/SmallVector.h"
-#include "llvm/Support/CommandLine.h"
-#include "llvm/Support/FileSystem.h"
-#include "llvm/Support/FormatVariadic.h"
-#include "llvm/Support/InitLLVM.h"
-#include "llvm/Support/Path.h"
-#include "llvm/Support/Timer.h"
-#include "llvm/Support/WithColor.h"
-#include "llvm/Support/raw_ostream.h"
-#include <memory>
-#include <string>
-#include <system_error>
-#include <vector>
-
-using namespace llvm;
-using namespace clang::ssaf;
-
-namespace fs = llvm::sys::fs;
-namespace path = llvm::sys::path;
-
-namespace {
-
-//===----------------------------------------------------------------------===//
-// Command-Line Options
-//===----------------------------------------------------------------------===//
-
-cl::OptionCategory SsafAnalyzerCategory("clang-ssaf-analyzer options");
-
-cl::list<std::string> LoadPlugins("load",
-                                  cl::desc("Load a plugin shared library"),
-                                  cl::value_desc("path"),
-                                  cl::cat(SsafAnalyzerCategory));
-
-cl::opt<std::string> LUSummaryPath(cl::Positional, cl::desc("<lu-summary>"),
-                                   cl::cat(SsafAnalyzerCategory));
-
-cl::opt<std::string> OutputPath("o", cl::desc("Output WPASuite path"),
-                                cl::value_desc("path"), cl::Required,
-                                cl::cat(SsafAnalyzerCategory));
-
-cl::list<std::string>
-    AnalysisNameStrs("analysis",
-                     cl::desc("Name of an analysis to run (may be repeated; "
-                              "default: run all registered analyses)"),
-                     cl::value_desc("name"), cl::cat(SsafAnalyzerCategory));
-
-cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
-                      cl::init(false), cl::cat(SsafAnalyzerCategory));
-
-cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
-                   cl::cat(SsafAnalyzerCategory));
-
-//===----------------------------------------------------------------------===//
-// Error Messages
-//===----------------------------------------------------------------------===//
-
-namespace LocalErrorMessages {
-
-constexpr const char *RunningAnalysis = "Running analysis '{0}'";
-
-} // namespace LocalErrorMessages
-
-//===----------------------------------------------------------------------===//
-// Diagnostic Utilities
-//===----------------------------------------------------------------------===//
-
-constexpr unsigned IndentationWidth = 2;
-
-template <typename... Ts>
-void info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
-  if (Verbose) {
-    llvm::WithColor::note()
-        << std::string(IndentationLevel * IndentationWidth, ' ') << "- "
-        << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
-  }
-}
-
-//===----------------------------------------------------------------------===//
-// Data Structures
-//===----------------------------------------------------------------------===//
-
-struct AnalyzerInput {
-  SummaryFile LUSummaryFile;
-  SummaryFile WPAOutputFile;
-  std::vector<AnalysisName> Names; // Empty means run all registered analyses.
-};
-
-//===----------------------------------------------------------------------===//
-// Pipeline
-//===----------------------------------------------------------------------===//
-
-AnalyzerInput validate(llvm::TimerGroup &TG) {
-  llvm::Timer TValidate("validate", "Validate Input", TG);
-  llvm::TimeRegion _(Time ? &TValidate : nullptr);
-
-  AnalyzerInput AI;
-
-  // Validate the LUSummary input path.
-  {
-    if (LUSummaryPath.empty()) {
-      fail("no LUSummary file specified");
-    }
-
-    llvm::SmallString<256> RealInputPath;
-    if (std::error_code EC =
-            fs::real_path(LUSummaryPath, RealInputPath, /*expand_tilde=*/true))
-      fail(ErrorMessages::CannotValidateSummary, LUSummaryPath, EC.message());
-
-    AI.LUSummaryFile = SummaryFile::fromPath(RealInputPath);
-  }
-
-  info(2, "Validated LUSummary input path '{0}'.", AI.LUSummaryFile.Path);
-
-  // Validate the WPASuite output path.
-  {
-    llvm::StringRef ParentDir = path::parent_path(OutputPath);
-    llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
-
-    if (!fs::exists(DirToCheck)) {
-      fail(ErrorMessages::CannotValidateSummary, OutputPath,
-           ErrorMessages::OutputDirectoryMissing);
-    }
-
-    // The output file does not exist yet, so real_path cannot be called on it
-    // directly. Resolve the parent directory first, then append the filename.
-    llvm::SmallString<256> RealParentDir;
-    if (std::error_code EC = fs::real_path(DirToCheck, RealParentDir))
-      fail(ErrorMessages::CannotValidateSummary, OutputPath, EC.message());
-
-    llvm::SmallString<256> RealOutputPath = RealParentDir;
-    path::append(RealOutputPath, path::filename(OutputPath));
-
-    AI.WPAOutputFile = SummaryFile::fromPath(RealOutputPath);
-  }
-
-  info(2, "Validated WPASuite output path '{0}'.", AI.WPAOutputFile.Path);
-
-  // Convert analysis name strings to AnalysisName objects.
-  for (const auto &Name : AnalysisNameStrs)
-    AI.Names.emplace_back(Name);
-
-  if (AI.Names.empty())
-    info(2, "No analyses specified; all registered analyses will be run.");
-  else
-    info(2, "Running {0} named {1}.", AI.Names.size(),
-         AI.Names.size() == 1 ? "analysis" : "analyses");
-
-  return AI;
-}
-
-void analyze(const AnalyzerInput &AI, llvm::TimerGroup &TG) {
-  llvm::Timer TRead("read", "Read LUSummary", TG);
-  llvm::Timer TRun("run", "Run Analyses", TG);
-  llvm::Timer TWrite("write", "Write WPASuite", TG);
-
-  // Read the LUSummary.
-  std::unique_ptr<LUSummary> LU;
-  {
-    info(2, "Reading LUSummary from '{0}'.", AI.LUSummaryFile.Path);
-    llvm::TimeRegion _(Time ? &TRead : nullptr);
-
-    auto ExpectedLU =
-        AI.LUSummaryFile.Format->readLUSummary(AI.LUSummaryFile.Path);
-    if (!ExpectedLU)
-      fail(ExpectedLU.takeError());
-
-    LU = std::make_unique<LUSummary>(std::move(*ExpectedLU));
-  }
-
-  // Run analyses.
-  WPASuite Suite;
-  {
-    info(2, "Running analyses.");
-    llvm::TimeRegion _(Time ? &TRun : nullptr);
-
-    AnalysisDriver Driver(std::move(LU));
-
-    llvm::Expected<WPASuite> ExpectedSuite =
-        AI.Names.empty() ? std::move(Driver).run() : Driver.run(AI.Names);
-    if (!ExpectedSuite)
-      fail(ExpectedSuite.takeError());
-
-    Suite = std::move(*ExpectedSuite);
-  }
-
-  // Write the WPASuite.
-  {
-    info(2, "Writing WPASuite to '{0}'.", AI.WPAOutputFile.Path);
-    llvm::TimeRegion _(Time ? &TWrite : nullptr);
-
-    if (auto Err = AI.WPAOutputFile.Format->writeWPASuite(
-            Suite, AI.WPAOutputFile.Path))
-      fail(std::move(Err));
-  }
-}
-
-} // namespace
-
-//===----------------------------------------------------------------------===//
-// Driver
-//===----------------------------------------------------------------------===//
-
-int main(int argc, const char **argv) {
-  llvm::StringRef ToolHeading = "SSAF Analyzer";
-
-  InitLLVM X(argc, argv);
-  initTool(argc, argv, "0.1", SsafAnalyzerCategory, ToolHeading);
-
-  loadPlugins(LoadPlugins);
-
-  llvm::TimerGroup AnalyzerTimers(getToolName(), ToolHeading);
-
-  {
-    info(0, "Analysis started.");
-
-    AnalyzerInput AI;
-
-    {
-      info(1, "Validating input.");
-      AI = validate(AnalyzerTimers);
-    }
-
-    {
-      info(1, "Running analyses.");
-      analyze(AI, AnalyzerTimers);
-    }
-
-    info(0, "Analysis finished.");
-  }
-
-  return 0;
-}

>From 5cba9db4b4dc697622eca058097ce6b60ce43fc2 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 11:59:00 -0700
Subject: [PATCH 20/30] Format properly

---
 .../Core/Serialization/JSONFormat/JSONFormatImpl.h              | 2 +-
 .../Serialization/JSONFormatTest/WPASuiteTest.cpp               | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
index 97154b19bed31..31a605efe90a5 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.h
@@ -16,12 +16,12 @@
 
 #include "../../ModelStringConversions.h"
 #include "JSONEntitySummaryEncoding.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/EntityLinker/EntitySummaryEncoding.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityLinkage.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/ErrorBuilder.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/Support/FormatProviders.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/StringExtras.h"
 #include "llvm/Support/ErrorHandling.h"
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
index 3972604a36f49..2517886ccd4d3 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
@@ -13,10 +13,10 @@
 
 #include "JSONFormatTest.h"
 
+#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
 #include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Testing/Support/Error.h"

>From 772c209cc5ba5383845bb29bb3008774e61042b8 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 13:07:10 -0700
Subject: [PATCH 21/30] Fix

---
 .../Core/Serialization/JSONFormat/WPASuite.cpp            | 4 ++--
 .../Serialization/JSONFormatTest/WPASuiteTest.cpp         | 8 +++-----
 2 files changed, 5 insertions(+), 7 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
index f6673d9ef4742..1d1e442e63c92 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/WPASuite.cpp
@@ -28,7 +28,7 @@ JSONFormat::analysisResultMapEntryFromJSON(const Object &Entry) const {
 
   AnalysisName Name = analysisNameFromJSON(*OptName);
 
-  auto ExpectedFns = AnalysisResultRegistryGenerator::lookup(Name);
+  auto ExpectedFns = AnalysisResultRegistry::lookup(Name);
   if (!ExpectedFns) {
     return ExpectedFns.takeError();
   }
@@ -53,7 +53,7 @@ JSONFormat::analysisResultMapEntryFromJSON(const Object &Entry) const {
 llvm::Expected<Object> JSONFormat::analysisResultMapEntryToJSON(
     const AnalysisName &Name,
     const std::unique_ptr<AnalysisResult> &Result) const {
-  auto ExpectedFns = AnalysisResultRegistryGenerator::lookup(Name);
+  auto ExpectedFns = AnalysisResultRegistry::lookup(Name);
   if (!ExpectedFns) {
     return ExpectedFns.takeError();
   }
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
index 2517886ccd4d3..5c6a3a7b7a925 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
@@ -74,8 +74,7 @@ deserializeTagsAnalysisResult(const json::Object &Obj,
   return std::move(R);
 }
 
-JSONFormat::AnalysisResultRegistryGenerator::Add<
-    TagsAnalysisResultForJSONFormatTest>
+JSONFormat::AnalysisResultRegistry::Add<TagsAnalysisResultForJSONFormatTest>
     RegisterTagsAnalysisFormatInfo(serializeTagsAnalysisResult,
                                    deserializeTagsAnalysisResult);
 
@@ -138,8 +137,7 @@ deserializeCountsAnalysisResult(const json::Object &Obj,
   return std::move(R);
 }
 
-JSONFormat::AnalysisResultRegistryGenerator::Add<
-    CountsAnalysisResultForJSONFormatTest>
+JSONFormat::AnalysisResultRegistry::Add<CountsAnalysisResultForJSONFormatTest>
     RegisterCountsAnalysisFormatInfo(serializeCountsAnalysisResult,
                                      deserializeCountsAnalysisResult);
 
@@ -167,7 +165,7 @@ deserializeFailingDeserializerAnalysisResult(const json::Object &,
                            "intentional deserializer failure");
 }
 
-JSONFormat::AnalysisResultRegistryGenerator::Add<
+JSONFormat::AnalysisResultRegistry::Add<
     FailingDeserializerAnalysisResultForJSONFormatTest>
     RegisterFailingDeserializerAnalysisFormatInfo(
         serializeFailingDeserializerAnalysisResult,

>From fa6bf39cc21da99f06845a78f4c4d808ce9c4b73 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 13:45:02 -0700
Subject: [PATCH 22/30] Fix

---
 .../Core/Serialization/SerializationFormat.h  | 69 ++++---------------
 .../JSONFormat/JSONFormatImpl.cpp             |  2 -
 2 files changed, 12 insertions(+), 59 deletions(-)

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
index 113e8ca9b1497..03f8f63899ef8 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
@@ -84,69 +84,24 @@ class SerializationFormat {
   static auto &get##FIELD_NAME(CLASS &X) { return X.FIELD_NAME; }
 #include "clang/ScalableStaticAnalysisFramework/Core/Model/PrivateFieldNames.def"
 
-  /// Generates a per-format plugin registry for analysis result
-  /// serializers and deserializers.
+  /// Per-format plugin registry for analysis result (de)serializers.
   ///
   /// Each concrete format (e.g. JSONFormat) instantiates this template once
-  /// via a \c using alias, then exposes that alias publicly so that analysis
-  /// authors can register (de)serialization support with a single declaration:
+  /// via a public \c using alias. Analysis authors register support with:
   ///
-  ///   static MyFormat::AnalysisResultRegistryGenerator::Add<MyAnalysisResult>
+  ///   static MyFormat::AnalysisResultRegistry::Add<MyAnalysisResult>
   ///       Reg(serializeFn, deserializeFn);
   ///
-  /// ---
-  /// Design overview
-  /// ---
+  /// \p FormatT is otherwise unused — it exists because \c llvm::Registry
+  /// is keyed on the \c Entry type, so two formats that happen to share the
+  /// same serializer/deserializer signatures would collide without a
+  /// disambiguating template parameter.
   ///
-  /// **Registry isolation via \p FormatT.**
-  /// The underlying store is \c llvm::Registry<Entry>, which is a global
-  /// linked list keyed on the \c Entry type. Because \c Entry is a member of
-  /// this template, each \c (FormatT, SerializerFn, DeserializerFn)
-  /// instantiation produces a distinct \c Entry type and therefore a distinct
-  /// \c llvm::Registry — even if two formats happen to share the same
-  /// serializer/deserializer function signatures. The \p FormatT parameter
-  /// exists solely to provide this isolation; it is otherwise unused inside
-  /// the template body.
-  ///
-  /// **Bridging \c function_ref into \c llvm::Registry.**
-  /// \c llvm::function_ref is a non-owning view of a callable — it cannot be
-  /// stored inside the registry because the registry only keeps nullary
-  /// factories of the form <tt>[]{ return make_unique<ConcreteEntry>(); }</tt>
-  /// that capture no state. Two mechanisms bridge this gap:
-  ///
-  ///   1. *Function-local statics as per-analysis storage.*
-  ///      Inside \c Add<AnalysisResultT>::Add(...), two function-local statics
-  ///      — \c SavedSerialize and \c SavedDeserialize — are initialized from
-  ///      the constructor arguments on the first (and only) call. Because
-  ///      \c Add<T>::Add(...) is a distinct function for each \c T, each
-  ///      analysis type gets its own pair of statics with program lifetime,
-  ///      giving the \c function_ref values a stable home.
-  ///
-  ///   2. *\c ConcreteEntry as a local struct.*
-  ///      \c ConcreteEntry is defined inside the \c Add constructor body so
-  ///      that its default constructor can read \c SavedSerialize and
-  ///      \c SavedDeserialize from the enclosing function scope. When
-  ///      \c llvm::Registry later calls \c E.instantiate() during \c lookup,
-  ///      it invokes \c ConcreteEntry(), which re-wraps those stored values
-  ///      into a fresh \c Entry — reconstructing the \c function_ref from the
-  ///      same stable underlying callables.
-  ///
-  /// **One-time registration via a function-local static \c Reg.**
-  /// \c static typename RegistryT::template Add<ConcreteEntry> Reg(NameStr,"")
-  /// is also a function-local static. C++ guarantees it is initialized exactly
-  /// once, on the first call to \c Add<T>::Add(...). Its constructor appends a
-  /// node to the \c llvm::Registry linked list, associating \c NameStr with
-  /// the \c ConcreteEntry factory. All subsequent calls to \c Add<T>::Add(...)
-  /// hit the \c Registered guard first and abort with a fatal error, making
-  /// duplicate registrations a detectable programmer mistake rather than a
-  /// silent no-op.
-  ///
-  /// **Lookup.**
-  /// \c lookup iterates \c RegistryT::entries() and compares each node's name
-  /// (available directly on the entry node without instantiation) against the
-  /// requested \c AnalysisName. Only the matching node is instantiated via
-  /// \c E.instantiate(), which invokes \c ConcreteEntry() and returns the
-  /// stored \c function_ref pair.
+  /// \c function_ref is non-owning, but \c llvm::Registry only stores
+  /// nullary factories (no captured state). Function-local statics inside
+  /// \c Add<T>::Add(...) give each analysis's \c function_ref values a
+  /// stable, program-lifetime home, and a local \c ConcreteEntry struct
+  /// reads them back when the registry instantiates the factory.
   template <class FormatT, class SerializerFn, class DeserializerFn>
   class AnalysisResultRegistryGenerator {
   public:
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
index 230fed5bac20d..1238192628cb2 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -13,9 +13,7 @@
 
 // NOLINTNEXTLINE(misc-use-internal-linkage)
 volatile int SSAFJSONFormatAnchorSource = 0;
-
 LLVM_INSTANTIATE_REGISTRY(llvm::Registry<clang::ssaf::JSONFormat::FormatInfo>)
-
 LLVM_INSTANTIATE_REGISTRY(
     llvm::Registry<clang::ssaf::JSONFormat::AnalysisResultRegistry::Entry>)
 

>From 53a0b478d34dd4fafbff2e5eee2ed1ccd1bdc88e Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 14:15:46 -0700
Subject: [PATCH 23/30] Remove unittests

---
 .../CMakeLists.txt                            |   1 -
 .../JSONFormatTest/WPASuiteTest.cpp           | 766 ------------------
 2 files changed, 767 deletions(-)
 delete mode 100644 clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp

diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 40e81f19ff4e4..8eeaa982daaf6 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -19,7 +19,6 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Serialization/JSONFormatTest/JSONFormatTest.cpp
   Serialization/JSONFormatTest/LUSummaryTest.cpp
   Serialization/JSONFormatTest/TUSummaryTest.cpp
-  Serialization/JSONFormatTest/WPASuiteTest.cpp
   SummaryData/SummaryDataTest.cpp
   SummaryNameTest.cpp
   TestFixture.cpp
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp b/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
deleted file mode 100644
index 5c6a3a7b7a925..0000000000000
--- a/clang/unittests/ScalableStaticAnalysisFramework/Serialization/JSONFormatTest/WPASuiteTest.cpp
+++ /dev/null
@@ -1,766 +0,0 @@
-//===- WPASuiteTest.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 SSAF JSON serialization format reading and writing of
-// WPASuite.
-//
-//===----------------------------------------------------------------------===//
-
-#include "JSONFormatTest.h"
-
-#include "clang/ScalableStaticAnalysisFramework/Core/Serialization/JSONFormat.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisName.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/AnalysisResult.h"
-#include "clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
-#include "llvm/ADT/STLExtras.h"
-#include "llvm/Support/FormatVariadic.h"
-#include "llvm/Testing/Support/Error.h"
-#include "gmock/gmock.h"
-
-#include <memory>
-#include <vector>
-
-using namespace clang::ssaf;
-using namespace llvm;
-using ::testing::AllOf;
-using ::testing::HasSubstr;
-
-namespace {
-
-// ============================================================================
-// First Test AnalysisResult - Tags (no entity ID references)
-// ============================================================================
-
-struct TagsAnalysisResultForJSONFormatTest final : AnalysisResult {
-  static AnalysisName analysisName() {
-    return AnalysisName("TagsAnalysisResultForJSONFormatTest");
-  }
-
-  std::vector<std::string> Tags;
-};
-
-json::Object serializeTagsAnalysisResult(const AnalysisResult &Result,
-                                         JSONFormat::EntityIdToJSONFn) {
-  const auto &R =
-      static_cast<const TagsAnalysisResultForJSONFormatTest &>(Result);
-  json::Array TagsArray;
-  for (const auto &Tag : R.Tags)
-    TagsArray.push_back(Tag);
-  return json::Object{{"tags", std::move(TagsArray)}};
-}
-
-Expected<std::unique_ptr<AnalysisResult>>
-deserializeTagsAnalysisResult(const json::Object &Obj,
-                              JSONFormat::EntityIdFromJSONFn) {
-  const json::Array *TagsArray = Obj.getArray("tags");
-  if (!TagsArray)
-    return createStringError(inconvertibleErrorCode(),
-                             "missing or invalid field 'tags'");
-
-  auto R = std::make_unique<TagsAnalysisResultForJSONFormatTest>();
-  for (const auto &[Index, Val] : llvm::enumerate(*TagsArray)) {
-    auto S = Val.getAsString();
-    if (!S)
-      return createStringError(inconvertibleErrorCode(),
-                               "tags element at index %zu is not a string",
-                               Index);
-    R->Tags.push_back(S->str());
-  }
-  return std::move(R);
-}
-
-JSONFormat::AnalysisResultRegistry::Add<TagsAnalysisResultForJSONFormatTest>
-    RegisterTagsAnalysisFormatInfo(serializeTagsAnalysisResult,
-                                   deserializeTagsAnalysisResult);
-
-// ============================================================================
-// Second Test AnalysisResult - Counts (with entity ID references)
-// ============================================================================
-
-struct CountsAnalysisResultForJSONFormatTest final : AnalysisResult {
-  static AnalysisName analysisName() {
-    return AnalysisName("CountsAnalysisResultForJSONFormatTest");
-  }
-
-  std::vector<std::pair<EntityId, int>> Counts;
-};
-
-json::Object
-serializeCountsAnalysisResult(const AnalysisResult &Result,
-                              JSONFormat::EntityIdToJSONFn ToJSON) {
-  const auto &R =
-      static_cast<const CountsAnalysisResultForJSONFormatTest &>(Result);
-  json::Array CountsArray;
-  for (const auto &[EI, Count] : R.Counts) {
-    CountsArray.push_back(
-        json::Object{{"entity_id", ToJSON(EI)}, {"count", Count}});
-  }
-  return json::Object{{"counts", std::move(CountsArray)}};
-}
-
-Expected<std::unique_ptr<AnalysisResult>>
-deserializeCountsAnalysisResult(const json::Object &Obj,
-                                JSONFormat::EntityIdFromJSONFn FromJSON) {
-  const json::Array *CountsArray = Obj.getArray("counts");
-  if (!CountsArray)
-    return createStringError(inconvertibleErrorCode(),
-                             "missing or invalid field 'counts'");
-
-  auto R = std::make_unique<CountsAnalysisResultForJSONFormatTest>();
-  for (const auto &[Index, Val] : llvm::enumerate(*CountsArray)) {
-    const json::Object *Entry = Val.getAsObject();
-    if (!Entry)
-      return createStringError(inconvertibleErrorCode(),
-                               "counts element at index %zu is not an object",
-                               Index);
-    const json::Object *EIObj = Entry->getObject("entity_id");
-    if (!EIObj)
-      return createStringError(
-          inconvertibleErrorCode(),
-          "missing or invalid 'entity_id' field at index %zu", Index);
-    auto ExpectedEI = FromJSON(*EIObj);
-    if (!ExpectedEI)
-      return ExpectedEI.takeError();
-
-    auto CountVal = Entry->getInteger("count");
-    if (!CountVal)
-      return createStringError(inconvertibleErrorCode(),
-                               "missing or invalid 'count' field at index %zu",
-                               Index);
-    R->Counts.emplace_back(*ExpectedEI, static_cast<int>(*CountVal));
-  }
-  return std::move(R);
-}
-
-JSONFormat::AnalysisResultRegistry::Add<CountsAnalysisResultForJSONFormatTest>
-    RegisterCountsAnalysisFormatInfo(serializeCountsAnalysisResult,
-                                     deserializeCountsAnalysisResult);
-
-// ============================================================================
-// FailingDeserializerAnalysisResult - always returns an error on deserialize
-// ============================================================================
-
-struct FailingDeserializerAnalysisResultForJSONFormatTest final
-    : AnalysisResult {
-  static AnalysisName analysisName() {
-    return AnalysisName("FailingDeserializerAnalysisResultForJSONFormatTest");
-  }
-};
-
-json::Object
-serializeFailingDeserializerAnalysisResult(const AnalysisResult &,
-                                           JSONFormat::EntityIdToJSONFn) {
-  return json::Object{};
-}
-
-Expected<std::unique_ptr<AnalysisResult>>
-deserializeFailingDeserializerAnalysisResult(const json::Object &,
-                                             JSONFormat::EntityIdFromJSONFn) {
-  return createStringError(inconvertibleErrorCode(),
-                           "intentional deserializer failure");
-}
-
-JSONFormat::AnalysisResultRegistry::Add<
-    FailingDeserializerAnalysisResultForJSONFormatTest>
-    RegisterFailingDeserializerAnalysisFormatInfo(
-        serializeFailingDeserializerAnalysisResult,
-        deserializeFailingDeserializerAnalysisResult);
-
-// ============================================================================
-// JSONFormatWPASuiteTest Fixture
-// ============================================================================
-
-class JSONFormatWPASuiteTest : public JSONFormatTest {
-protected:
-  llvm::Expected<WPASuite>
-  readWPASuiteFromString(StringRef JSON,
-                         StringRef FileName = "test.json") const {
-    auto ExpectedFilePath = writeJSON(JSON, FileName);
-    if (!ExpectedFilePath)
-      return ExpectedFilePath.takeError();
-    return JSONFormat().readWPASuite(makePath(FileName));
-  }
-
-  llvm::Expected<WPASuite> readWPASuiteFromFile(StringRef FileName) const {
-    return JSONFormat().readWPASuite(makePath(FileName));
-  }
-
-  llvm::Error writeWPASuite(const WPASuite &Suite, StringRef FileName) const {
-    return JSONFormat().writeWPASuite(Suite, makePath(FileName));
-  }
-
-  // Builds an empty WPASuite (no id_table entries, no results) and writes it.
-  llvm::Error writeEmptyWPASuite(StringRef FileName) const {
-    WPASuite Suite = makeWPASuite();
-    return writeWPASuite(Suite, FileName);
-  }
-
-  void readWriteCompare(StringRef JSON) const {
-    const PathString InputFileName("input.json");
-    const PathString OutputFileName("output.json");
-
-    auto ExpectedInputFilePath = writeJSON(JSON, InputFileName);
-    ASSERT_THAT_EXPECTED(ExpectedInputFilePath, Succeeded());
-
-    auto ExpectedSuite = readWPASuiteFromFile(InputFileName);
-    ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
-
-    ASSERT_THAT_ERROR(writeWPASuite(*ExpectedSuite, OutputFileName),
-                      Succeeded());
-
-    auto ExpectedInputJSON = readJSONFromFile(InputFileName);
-    ASSERT_THAT_EXPECTED(ExpectedInputJSON, Succeeded());
-
-    auto ExpectedOutputJSON = readJSONFromFile(OutputFileName);
-    ASSERT_THAT_EXPECTED(ExpectedOutputJSON, Succeeded());
-
-    auto ExpectedNormalizedInput =
-        normalizeWPASuiteJSON(std::move(*ExpectedInputJSON));
-    ASSERT_THAT_EXPECTED(ExpectedNormalizedInput, Succeeded());
-
-    auto ExpectedNormalizedOutput =
-        normalizeWPASuiteJSON(std::move(*ExpectedOutputJSON));
-    ASSERT_THAT_EXPECTED(ExpectedNormalizedOutput, Succeeded());
-
-    ASSERT_EQ(*ExpectedNormalizedInput, *ExpectedNormalizedOutput)
-        << "Serialization is broken: input is different from output\n"
-        << "Input:  " << llvm::formatv("{0:2}", *ExpectedNormalizedInput).str()
-        << "\n"
-        << "Output: "
-        << llvm::formatv("{0:2}", *ExpectedNormalizedOutput).str();
-  }
-
-private:
-  static llvm::Expected<json::Value> normalizeWPASuiteJSON(json::Value Val) {
-    auto *Obj = Val.getAsObject();
-    if (!Obj)
-      return createStringError(
-          inconvertibleErrorCode(),
-          "Cannot normalize WPASuite JSON: expected object");
-
-    auto *IdTable = Obj->getArray("id_table");
-    if (!IdTable)
-      return createStringError(
-          inconvertibleErrorCode(),
-          "Cannot normalize WPASuite JSON: missing 'id_table' array");
-
-    llvm::sort(*IdTable, [](const json::Value &A, const json::Value &B) {
-      const auto *OA = A.getAsObject();
-      const auto *OB = B.getAsObject();
-      if (!OA || !OB)
-        return false;
-      auto IA = OA->getInteger("id");
-      auto IB = OB->getInteger("id");
-      return IA && IB && *IA < *IB;
-    });
-
-    auto *Results = Obj->getArray("results");
-    if (!Results)
-      return createStringError(
-          inconvertibleErrorCode(),
-          "Cannot normalize WPASuite JSON: missing 'results' array");
-
-    llvm::sort(*Results, [](const json::Value &A, const json::Value &B) {
-      const auto *OA = A.getAsObject();
-      const auto *OB = B.getAsObject();
-      if (!OA || !OB)
-        return false;
-      auto NA = OA->getString("analysis_name");
-      auto NB = OB->getString("analysis_name");
-      return NA && NB && *NA < *NB;
-    });
-
-    return Val;
-  }
-};
-
-// ============================================================================
-// readJSON() Error Tests
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, NonexistentFile) {
-  auto Result = readWPASuiteFromFile("nonexistent.json");
-
-  EXPECT_THAT_EXPECTED(
-      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from"),
-                                      HasSubstr("file does not exist"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, PathIsDirectory) {
-  auto ExpectedDirPath = makeDirectory("test_directory.json");
-  ASSERT_THAT_EXPECTED(ExpectedDirPath, Succeeded());
-
-  auto Result = readWPASuiteFromFile("test_directory.json");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from"),
-                              HasSubstr("path is a directory, not a file"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, NotJsonExtension) {
-  auto ExpectedFilePath = writeJSON("{}", "test.txt");
-  ASSERT_THAT_EXPECTED(ExpectedFilePath, Succeeded());
-
-  auto Result = JSONFormat().readWPASuite(makePath("test.txt"));
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read file"),
-                              HasSubstr("file does not end with '.json'"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, InvalidSyntax) {
-  auto Result = readWPASuiteFromString("{ invalid json }");
-
-  EXPECT_THAT_EXPECTED(
-      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                                      HasSubstr("Expected object key"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, NotObject) {
-  auto Result = readWPASuiteFromString("[]");
-
-  EXPECT_THAT_EXPECTED(
-      Result, FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                                      HasSubstr("failed to read WPASuite"),
-                                      HasSubstr("expected JSON object"))));
-}
-
-// ============================================================================
-// Structural Error Tests - id_table
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, MissingIdTable) {
-  auto Result = readWPASuiteFromString(R"({"results": []})");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read IdTable from field "
-                                        "'id_table'"),
-                              HasSubstr("expected JSON array"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, IdTableNotArray) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": {},
-    "results": []
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read IdTable from field "
-                                        "'id_table'"),
-                              HasSubstr("expected JSON array"))));
-}
-
-// ============================================================================
-// Structural Error Tests - results
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, MissingResults) {
-  auto Result = readWPASuiteFromString(R"({"id_table": []})");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read WPA results from field "
-                                        "'results'"),
-                              HasSubstr("expected JSON array"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultsNotArray) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": {}
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read WPA results from field "
-                                        "'results'"),
-                              HasSubstr("expected JSON array"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryNotObject) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": ["invalid"]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("reading WPA results from field "
-                                        "'results'"),
-                              HasSubstr("failed to read WPA result entry from "
-                                        "index '0'"),
-                              HasSubstr("expected JSON object"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryMissingAnalysisName) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [{"result": {}}]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("reading WPA results from field "
-                                        "'results'"),
-                              HasSubstr("reading WPA result entry from index "
-                                        "'0'"),
-                              HasSubstr("failed to read AnalysisName from "
-                                        "field 'analysis_name'"),
-                              HasSubstr("expected JSON string"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryAnalysisNameNotString) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [{"analysis_name": 42, "result": {}}]
-  })");
-
-  EXPECT_THAT_EXPECTED(Result, FailedWithMessage(AllOf(
-                                   HasSubstr("reading WPASuite from file"),
-                                   HasSubstr("failed to read AnalysisName from "
-                                             "field 'analysis_name'"),
-                                   HasSubstr("expected JSON string"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryNoFormatInfo) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {"analysis_name": "UnregisteredAnalysis", "result": {}}
-    ]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result, FailedWithMessage(
-                  AllOf(HasSubstr("reading WPASuite from file"),
-                        HasSubstr("reading WPA results from field 'results'"),
-                        HasSubstr("reading WPA result entry from index '0'"),
-                        HasSubstr("no support registered for analysis: "
-                                  "AnalysisName(UnregisteredAnalysis)"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryMissingResultField) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {"analysis_name": "TagsAnalysisResultForJSONFormatTest"}
-    ]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("reading WPA results from field "
-                                        "'results'"),
-                              HasSubstr("reading WPA result entry from index "
-                                        "'0'"),
-                              HasSubstr("failed to read AnalysisResult from "
-                                        "field 'result'"),
-                              HasSubstr("expected JSON object"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryResultNotObject) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
-       "result": "not_an_object"}
-    ]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("failed to read AnalysisResult from "
-                                        "field 'result'"),
-                              HasSubstr("expected JSON object"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, ResultEntryDeserializerError) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {"analysis_name": "FailingDeserializerAnalysisResultForJSONFormatTest",
-       "result": {}}
-    ]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(HasSubstr("reading WPASuite from file"),
-                              HasSubstr("reading WPA results from field "
-                                        "'results'"),
-                              HasSubstr("reading WPA result entry from index "
-                                        "'0'"),
-                              HasSubstr("intentional deserializer failure"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, DuplicateAnalysisName) {
-  auto Result = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
-       "result": {"tags": []}},
-      {"analysis_name": "TagsAnalysisResultForJSONFormatTest",
-       "result": {"tags": []}}
-    ]
-  })");
-
-  EXPECT_THAT_EXPECTED(
-      Result,
-      FailedWithMessage(AllOf(
-          HasSubstr("reading WPASuite from file"),
-          HasSubstr("reading WPA results from field 'results'"),
-          HasSubstr("failed to insert WPA result at index '1'"),
-          HasSubstr("encountered duplicate "
-                    "'AnalysisName(TagsAnalysisResultForJSONFormatTest)'"))));
-}
-
-// ============================================================================
-// Write Error Tests
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, WriteFileAlreadyExists) {
-  auto ExpectedFilePath = writeJSON("{}", "existing.json");
-  ASSERT_THAT_EXPECTED(ExpectedFilePath, Succeeded());
-
-  auto Result = writeEmptyWPASuite("existing.json");
-
-  EXPECT_THAT_ERROR(
-      std::move(Result),
-      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
-                              HasSubstr("failed to write file"),
-                              HasSubstr("file already exists"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, WriteParentDirectoryNotFound) {
-  PathString FilePath = makePath("nonexistent-dir", "test.json");
-
-  auto Result = JSONFormat().writeWPASuite(makeWPASuite(), FilePath);
-
-  EXPECT_THAT_ERROR(
-      std::move(Result),
-      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
-                              HasSubstr("failed to write file"),
-                              HasSubstr("parent directory does not exist"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, WriteNotJsonExtension) {
-  auto Result =
-      JSONFormat().writeWPASuite(makeWPASuite(), makePath("test.txt"));
-
-  EXPECT_THAT_ERROR(
-      std::move(Result),
-      FailedWithMessage(AllOf(HasSubstr("writing WPASuite to file"),
-                              HasSubstr("failed to write file"),
-                              HasSubstr("file does not end with '.json'"))));
-}
-
-TEST_F(JSONFormatWPASuiteTest, WriteNoFormatInfo) {
-  WPASuite Suite = makeWPASuite();
-  getData(Suite).emplace(
-      AnalysisName("UnregisteredAnalysisForJSONFormatTest"),
-      std::make_unique<TagsAnalysisResultForJSONFormatTest>());
-
-  auto Result = writeWPASuite(Suite, "output.json");
-
-  EXPECT_THAT_ERROR(
-      std::move(Result),
-      FailedWithMessage(AllOf(
-          HasSubstr("writing WPASuite to file"),
-          HasSubstr("no support registered for analysis: "
-                    "AnalysisName(UnregisteredAnalysisForJSONFormatTest)"))));
-}
-
-// ============================================================================
-// Round-Trip Tests
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, RoundTripEmpty) {
-  readWriteCompare(R"({
-    "id_table": [],
-    "results": []
-  })");
-}
-
-TEST_F(JSONFormatWPASuiteTest, RoundTripSingleResultNoEntities) {
-  readWriteCompare(R"({
-    "id_table": [],
-    "results": [
-      {
-        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
-        "result": {"tags": ["alpha", "beta", "gamma"]}
-      }
-    ]
-  })");
-}
-
-TEST_F(JSONFormatWPASuiteTest, RoundTripSingleResultWithEntityRefs) {
-  readWriteCompare(R"({
-    "id_table": [
-      {
-        "id": 0,
-        "name": {
-          "usr": "c:@F at foo",
-          "suffix": "",
-          "namespace": [
-            {"kind": "CompilationUnit", "name": "a.cpp"},
-            {"kind": "LinkUnit", "name": "a.exe"}
-          ]
-        }
-      },
-      {
-        "id": 1,
-        "name": {
-          "usr": "c:@F at bar",
-          "suffix": "",
-          "namespace": [
-            {"kind": "CompilationUnit", "name": "a.cpp"},
-            {"kind": "LinkUnit", "name": "a.exe"}
-          ]
-        }
-      }
-    ],
-    "results": [
-      {
-        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
-        "result": {
-          "counts": [
-            {"entity_id": {"@": 0}, "count": 42},
-            {"entity_id": {"@": 1}, "count": 7}
-          ]
-        }
-      }
-    ]
-  })");
-}
-
-TEST_F(JSONFormatWPASuiteTest, RoundTripMultipleResults) {
-  readWriteCompare(R"({
-    "id_table": [
-      {
-        "id": 0,
-        "name": {
-          "usr": "c:@F at foo",
-          "suffix": "",
-          "namespace": [
-            {"kind": "LinkUnit", "name": "test.exe"}
-          ]
-        }
-      }
-    ],
-    "results": [
-      {
-        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
-        "result": {
-          "counts": [
-            {"entity_id": {"@": 0}, "count": 100}
-          ]
-        }
-      },
-      {
-        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
-        "result": {"tags": ["important"]}
-      }
-    ]
-  })");
-}
-
-TEST_F(JSONFormatWPASuiteTest, RoundTripEmptyResultPayload) {
-  readWriteCompare(R"({
-    "id_table": [],
-    "results": [
-      {
-        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
-        "result": {"tags": []}
-      }
-    ]
-  })");
-}
-
-// ============================================================================
-// Content Verification Tests
-// ============================================================================
-
-TEST_F(JSONFormatWPASuiteTest, ReadVerifyTagsResult) {
-  auto ExpectedSuite = readWPASuiteFromString(R"({
-    "id_table": [],
-    "results": [
-      {
-        "analysis_name": "TagsAnalysisResultForJSONFormatTest",
-        "result": {"tags": ["foo", "bar"]}
-      }
-    ]
-  })");
-
-  ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
-  const WPASuite &Suite = *ExpectedSuite;
-
-  ASSERT_TRUE(
-      Suite.contains(TagsAnalysisResultForJSONFormatTest::analysisName()));
-  auto ExpectedResult = Suite.get<TagsAnalysisResultForJSONFormatTest>();
-  ASSERT_THAT_EXPECTED(ExpectedResult, Succeeded());
-
-  const auto &R = *ExpectedResult;
-  ASSERT_EQ(R.Tags.size(), 2u);
-  EXPECT_EQ(R.Tags[0], "foo");
-  EXPECT_EQ(R.Tags[1], "bar");
-}
-
-TEST_F(JSONFormatWPASuiteTest, ReadVerifyCountsResultWithEntityId) {
-  auto ExpectedSuite = readWPASuiteFromString(R"({
-    "id_table": [
-      {
-        "id": 0,
-        "name": {
-          "usr": "c:@F at foo",
-          "suffix": "",
-          "namespace": [
-            {"kind": "CompilationUnit", "name": "test.cpp"},
-            {"kind": "LinkUnit", "name": "test.exe"}
-          ]
-        }
-      }
-    ],
-    "results": [
-      {
-        "analysis_name": "CountsAnalysisResultForJSONFormatTest",
-        "result": {
-          "counts": [
-            {"entity_id": {"@": 0}, "count": 99}
-          ]
-        }
-      }
-    ]
-  })");
-
-  ASSERT_THAT_EXPECTED(ExpectedSuite, Succeeded());
-  const WPASuite &Suite = *ExpectedSuite;
-
-  ASSERT_TRUE(
-      Suite.contains(CountsAnalysisResultForJSONFormatTest::analysisName()));
-  auto ExpectedResult = Suite.get<CountsAnalysisResultForJSONFormatTest>();
-  ASSERT_THAT_EXPECTED(ExpectedResult, Succeeded());
-
-  const auto &R = *ExpectedResult;
-  ASSERT_EQ(R.Counts.size(), 1u);
-  EXPECT_EQ(R.Counts[0].second, 99);
-}
-
-} // namespace

>From 2ffa686276e5862627140a5e0b28be9461ebf3c2 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 14:45:33 -0700
Subject: [PATCH 24/30] Rename

---
 .../CMakeLists.txt                            |  2 +-
 .../{plugins => Plugins}/CMakeLists.txt       |  2 +-
 .../ExamplePlugin}/CMakeLists.txt             |  6 +++---
 .../ExamplePlugin/ExamplePlugin.cpp}          |  4 ++--
 .../Scalable/ssaf-analyzer/plugin.test        | 14 ++++++-------
 .../ssaf-format/wpa-suite/plugin.test         | 21 +++++++++----------
 6 files changed, 24 insertions(+), 25 deletions(-)
 rename clang/lib/ScalableStaticAnalysisFramework/{plugins => Plugins}/CMakeLists.txt (93%)
 rename clang/lib/ScalableStaticAnalysisFramework/{plugins/ssaf-wpa-suite-test-plugin => Plugins/ExamplePlugin}/CMakeLists.txt (85%)
 rename clang/lib/ScalableStaticAnalysisFramework/{plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp => Plugins/ExamplePlugin/ExamplePlugin.cpp} (98%)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
index a803d88cd978a..e09c44b7cfd52 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -1,5 +1,5 @@
 add_subdirectory(Analyses)
 add_subdirectory(Core)
 add_subdirectory(Frontend)
-add_subdirectory(plugins)
+add_subdirectory(Plugins)
 add_subdirectory(Tool)
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Plugins/CMakeLists.txt
similarity index 93%
rename from clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt
rename to clang/lib/ScalableStaticAnalysisFramework/Plugins/CMakeLists.txt
index 7dba9a4b8d9a0..97f4e4c01ab8f 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/plugins/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/CMakeLists.txt
@@ -8,5 +8,5 @@ if(LLVM_ENABLE_PLUGINS)
   # tool that will load it, and pull only include paths (not link deps) from
   # the libraries whose headers are needed. All symbols are resolved from the
   # tool's address space when the plugin is loaded via --load.
-  add_subdirectory(ssaf-wpa-suite-test-plugin)
+  add_subdirectory(ExamplePlugin)
 endif()
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
similarity index 85%
rename from clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt
rename to clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
index b78297b324253..2b6fba78c96a7 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
@@ -10,12 +10,12 @@
 # dynamic loader resolve every SSAF and LLVM symbol from the tool's address
 # space at load time.
 
-add_llvm_library(SSAFWPASuiteTestPlugin MODULE BUILDTREE_ONLY
-  SSAFWPASuiteTestPlugin.cpp
+add_llvm_library(SSAFExamplePlugin MODULE BUILDTREE_ONLY
+  ExamplePlugin.cpp
   PLUGIN_TOOL clang-ssaf-analyzer
   )
 
 # Pull in SSAF and LLVM include paths without any static link dependency.
-target_include_directories(SSAFWPASuiteTestPlugin PRIVATE
+target_include_directories(SSAFExamplePlugin PRIVATE
   $<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkCore,INTERFACE_INCLUDE_DIRECTORIES>
   )
diff --git a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
similarity index 98%
rename from clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp
rename to clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
index 465e6f1488908..4103d526dd24f 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/plugins/ssaf-wpa-suite-test-plugin/SSAFWPASuiteTestPlugin.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
@@ -1,4 +1,4 @@
-//===- SSAFWPASuiteTestPlugin.cpp - WPASuite serialization test plugin ----===//
+//===- ExamplePlugin.cpp - WPASuite serialization test plugin -------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -13,7 +13,7 @@
 // without depending on any real analysis logic.
 //
 // Usage:
-//   clang-ssaf-analyzer --load <path/to/SSAFWPASuiteTestPlugin.so> \
+//   clang-ssaf-analyzer --load <path/to/SSAFExamplePlugin.so> \
 //     --analysis TagsAnalysisResult \
 //     --analysis CountsAnalysisResult \
 //     -o output.json input.json
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test b/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
index b56446f510190..45a7a87efe502 100644
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
+++ b/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
@@ -1,5 +1,5 @@
-// Tests for clang-ssaf-analyzer WPASuite serialization via the test plugin.
-// The SSAFWPASuiteTestPlugin registers TagsAnalysisResult and
+// Tests for clang-ssaf-analyzer WPASuite serialization via the example plugin.
+// The SSAFExamplePlugin registers TagsAnalysisResult and
 // CountsAnalysisResult together with their JSON serializers so that the
 // round-trip through clang-ssaf-analyzer can be verified end-to-end.
 
@@ -13,7 +13,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   --analysis TagsAnalysisResult \
 // RUN:   -o %t/tags.json \
 // RUN:   %S/Inputs/lu-empty.json
@@ -30,7 +30,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   --analysis CountsAnalysisResult \
 // RUN:   -o %t/counts.json \
 // RUN:   %S/Inputs/lu-empty.json
@@ -44,7 +44,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   --analysis TagsAnalysisResult \
 // RUN:   --analysis CountsAnalysisResult \
 // RUN:   -o %t/both.json \
@@ -59,7 +59,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   -o %t/all.json \
 // RUN:   %S/Inputs/lu-empty.json
 // RUN: FileCheck %s --input-file %t/all.json --check-prefix=ALL-OUT
@@ -72,7 +72,7 @@
 // ============================================================================
 
 // RUN: not clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   --analysis UnknownAnalysis \
 // RUN:   -o %t/unknown.json \
 // RUN:   %S/Inputs/lu-empty.json 2>&1 \
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
index 45258aaf6f6d0..a80baf1206fbf 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
@@ -1,5 +1,4 @@
-// Tests for clang-ssaf-format --type wpa that require the WPASuite test
-// plugin. Covers:
+// Tests for clang-ssaf-format --type wpa that require the example plugin. Covers:
 //   - Read errors that need a registered deserializer to reach
 //   - Round-trip correctness (read → write → verify)
 //   - Content verification (field values in the output)
@@ -16,7 +15,7 @@
 // 'result' field is absent; the deserializer lookup succeeds but the field
 // check fires before the deserializer is called.
 // RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/result-entry-missing-result-field.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-RESULT-FIELD
 // NO-RESULT-FIELD:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
@@ -25,7 +24,7 @@
 
 // 'result' is a string, not an object.
 // RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/result-entry-result-not-object.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=RESULT-NOT-OBJ
 // RESULT-NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
@@ -33,7 +32,7 @@
 
 // Deserializer returns an error.
 // RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/result-entry-deserializer-error.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=DESER-ERR
 // DESER-ERR:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
@@ -42,7 +41,7 @@
 
 // Duplicate analysis name in results array.
 // RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/duplicate-analysis-name.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=DUPLICATE
 // DUPLICATE:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
@@ -54,7 +53,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/rt-empty.json \
 // RUN:   -o %t/rt-empty.json
 // RUN: FileCheck %s --input-file %t/rt-empty.json --check-prefix=RT-EMPTY
@@ -67,7 +66,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/rt-single-tags.json \
 // RUN:   -o %t/rt-single-tags.json
 // RUN: FileCheck %s --input-file %t/rt-single-tags.json --check-prefix=RT-TAGS
@@ -82,7 +81,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/rt-empty-payload.json \
 // RUN:   -o %t/rt-empty-payload.json
 // RUN: FileCheck %s --input-file %t/rt-empty-payload.json --check-prefix=RT-EMPTY-PAYLOAD
@@ -95,7 +94,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/rt-counts-with-entities.json \
 // RUN:   -o %t/rt-counts-with-entities.json
 // RUN: FileCheck %s --input-file %t/rt-counts-with-entities.json --check-prefix=RT-COUNTS
@@ -112,7 +111,7 @@
 // ============================================================================
 
 // RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFWPASuiteTestPlugin%pluginext \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
 // RUN:   %S/Inputs/rt-multiple-results.json \
 // RUN:   -o %t/rt-multiple-results.json
 // RUN: FileCheck %s --input-file %t/rt-multiple-results.json --check-prefix=RT-MULTI

>From 00d85e8883d63ca7314c814df033665e705ff9fa Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 15:07:40 -0700
Subject: [PATCH 25/30] More

---
 .../Core/Serialization/SerializationFormat.h  | 24 +++++-
 .../Plugins/ExamplePlugin/CMakeLists.txt      |  8 +-
 .../Plugins/ExamplePlugin/ExamplePlugin.cpp   | 52 ++++++------
 .../ssaf-analyzer/Inputs/lu-empty.json        | 11 ---
 .../Analysis/Scalable/ssaf-analyzer/cli.test  | 10 ---
 .../Analysis/Scalable/ssaf-analyzer/io.test   | 21 -----
 .../Scalable/ssaf-analyzer/plugin.test        | 80 -------------------
 .../ssaf-format/wpa-suite/errors.test         | 10 +--
 8 files changed, 56 insertions(+), 160 deletions(-)
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/io.test
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
index 03f8f63899ef8..b90411f910730 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
@@ -92,6 +92,9 @@ class SerializationFormat {
   ///   static MyFormat::AnalysisResultRegistry::Add<MyAnalysisResult>
   ///       Reg(serializeFn, deserializeFn);
   ///
+  /// The serializer receives \c const MyAnalysisResult & directly — the
+  /// \c Add wrapper handles the downcast from \c AnalysisResult internally.
+  ///
   /// \p FormatT is otherwise unused — it exists because \c llvm::Registry
   /// is keyed on the \c Entry type, so two formats that happen to share the
   /// same serializer/deserializer signatures would collide without a
@@ -116,14 +119,31 @@ class SerializationFormat {
     using RegistryT = llvm::Registry<Entry>;
 
     template <class AnalysisResultT> struct Add {
-      Add(SerializerFn Serialize, DeserializerFn Deserialize) {
+      /// Extracts the typed serializer signature from \c SerializerFn.
+      /// Given \c function_ref<R(const AnalysisResult &, Args...)>, produces
+      /// a function-pointer type \c R(*)(const AnalysisResultT &, Args...) and
+      /// a static \c wrap() that downcasts and forwards.
+      template <class> struct SerializerAdapter;
+      template <class R, class... Args>
+      struct SerializerAdapter<
+          llvm::function_ref<R(const AnalysisResult &, Args...)>> {
+        using TypedFnPtr = R (*)(const AnalysisResultT &, Args...);
+        static inline TypedFnPtr Saved = nullptr;
+        static R wrap(const AnalysisResult &Base, Args... args) {
+          return Saved(static_cast<const AnalysisResultT &>(Base), args...);
+        }
+      };
+      using SA = SerializerAdapter<SerializerFn>;
+
+      Add(typename SA::TypedFnPtr TypedSerialize, DeserializerFn Deserialize) {
         static bool Registered = false;
         if (Registered) {
           ErrorBuilder::fatal("support is already registered for analysis: {0}",
                               AnalysisResultT::analysisName());
         }
         Registered = true;
-        static SerializerFn SavedSerialize = Serialize;
+        SA::Saved = TypedSerialize;
+        static SerializerFn SavedSerialize(&SA::wrap);
         static DeserializerFn SavedDeserialize = Deserialize;
 
         struct ConcreteEntry : Entry {
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
index 2b6fba78c96a7..4654578dbad4c 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/CMakeLists.txt
@@ -1,8 +1,8 @@
 # Do NOT set LLVM_LINK_COMPONENTS here. Setting it would cause LLVM libraries
 # (e.g. Support) to be linked statically into this MODULE, producing a second
-# copy of LLVM in the process alongside the copy already loaded by
-# clang-ssaf-analyzer. Two copies means two separate llvm::Registry instances,
-# so registrations made here would not be visible to the host tool.
+# copy of LLVM in the process alongside the copy already loaded by the host
+# tool. Two copies means two separate llvm::Registry instances, so
+# registrations made here would not be visible to the host tool.
 #
 # The same applies to clang static libraries: do not use
 # clang_target_link_libraries here. Instead, expose only their include paths
@@ -12,7 +12,7 @@
 
 add_llvm_library(SSAFExamplePlugin MODULE BUILDTREE_ONLY
   ExamplePlugin.cpp
-  PLUGIN_TOOL clang-ssaf-analyzer
+  PLUGIN_TOOL clang-ssaf-format
   )
 
 # Pull in SSAF and LLVM include paths without any static link dependency.
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
index 4103d526dd24f..59b5b9f37f181 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
@@ -1,4 +1,4 @@
-//===- ExamplePlugin.cpp - WPASuite serialization test plugin -------------===//
+//===- ExamplePlugin.cpp - Example SSAF plugin ----------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -6,17 +6,10 @@
 //
 //===----------------------------------------------------------------------===//
 //
-// A loadable plugin for clang-ssaf-analyzer lit tests that exercises WPASuite
-// JSON serialization. It registers two analysis result types — Tags and Counts
-// — together with their JSON serializers/deserializers and trivial analysis
-// implementations, enabling end-to-end lit tests for the WPASuite round-trip
-// without depending on any real analysis logic.
-//
-// Usage:
-//   clang-ssaf-analyzer --load <path/to/SSAFExamplePlugin.so> \
-//     --analysis TagsAnalysisResult \
-//     --analysis CountsAnalysisResult \
-//     -o output.json input.json
+// A loadable plugin that registers three analysis result types — Tags, Counts,
+// and FailingDeserializer — together with their JSON serializers/deserializers
+// and trivial DerivedAnalysis implementations. Used by lit tests for both
+// clang-ssaf-analyzer and clang-ssaf-format.
 //
 //===----------------------------------------------------------------------===//
 
@@ -52,12 +45,12 @@ struct TagsAnalysisResult final : AnalysisResult {
   std::vector<std::string> Tags;
 };
 
-json::Object serializeTagsAnalysisResult(const AnalysisResult &Result,
+json::Object serializeTagsAnalysisResult(const TagsAnalysisResult &R,
                                          JSONFormat::EntityIdToJSONFn) {
-  const auto &R = static_cast<const TagsAnalysisResult &>(Result);
   json::Array TagsArray;
-  for (const auto &Tag : R.Tags)
+  for (const auto &Tag : R.Tags) {
     TagsArray.push_back(Tag);
+  }
   return json::Object{{"tags", std::move(TagsArray)}};
 }
 
@@ -65,17 +58,19 @@ Expected<std::unique_ptr<AnalysisResult>>
 deserializeTagsAnalysisResult(const json::Object &Obj,
                               JSONFormat::EntityIdFromJSONFn) {
   const json::Array *TagsArray = Obj.getArray("tags");
-  if (!TagsArray)
+  if (!TagsArray) {
     return createStringError(inconvertibleErrorCode(),
                              "missing or invalid field 'tags'");
+  }
 
   auto R = std::make_unique<TagsAnalysisResult>();
   for (const auto &[Index, Val] : llvm::enumerate(*TagsArray)) {
     auto S = Val.getAsString();
-    if (!S)
+    if (!S) {
       return createStringError(inconvertibleErrorCode(),
                                "tags element at index %zu is not a string",
                                Index);
+    }
     R->Tags.push_back(S->str());
   }
   return std::move(R);
@@ -121,9 +116,8 @@ struct CountsAnalysisResult final : AnalysisResult {
 };
 
 json::Object
-serializeCountsAnalysisResult(const AnalysisResult &Result,
+serializeCountsAnalysisResult(const CountsAnalysisResult &R,
                               JSONFormat::EntityIdToJSONFn ToJSON) {
-  const auto &R = static_cast<const CountsAnalysisResult &>(Result);
   json::Array CountsArray;
   for (const auto &[EI, Count] : R.Counts) {
     CountsArray.push_back(
@@ -136,31 +130,36 @@ Expected<std::unique_ptr<AnalysisResult>>
 deserializeCountsAnalysisResult(const json::Object &Obj,
                                 JSONFormat::EntityIdFromJSONFn FromJSON) {
   const json::Array *CountsArray = Obj.getArray("counts");
-  if (!CountsArray)
+  if (!CountsArray) {
     return createStringError(inconvertibleErrorCode(),
                              "missing or invalid field 'counts'");
+  }
 
   auto R = std::make_unique<CountsAnalysisResult>();
   for (const auto &[Index, Val] : llvm::enumerate(*CountsArray)) {
     const json::Object *Entry = Val.getAsObject();
-    if (!Entry)
+    if (!Entry) {
       return createStringError(inconvertibleErrorCode(),
                                "counts element at index %zu is not an object",
                                Index);
+    }
     const json::Object *EIObj = Entry->getObject("entity_id");
-    if (!EIObj)
+    if (!EIObj) {
       return createStringError(
           inconvertibleErrorCode(),
           "missing or invalid 'entity_id' field at index %zu", Index);
+    }
     auto ExpectedEI = FromJSON(*EIObj);
-    if (!ExpectedEI)
+    if (!ExpectedEI) {
       return ExpectedEI.takeError();
+    }
 
     auto CountVal = Entry->getInteger("count");
-    if (!CountVal)
+    if (!CountVal) {
       return createStringError(inconvertibleErrorCode(),
                                "missing or invalid 'count' field at index %zu",
                                Index);
+    }
     R->Counts.emplace_back(*ExpectedEI, static_cast<int>(*CountVal));
   }
   return std::move(R);
@@ -204,9 +203,8 @@ struct FailingDeserializerAnalysisResult final : AnalysisResult {
   }
 };
 
-json::Object
-serializeFailingDeserializerAnalysisResult(const AnalysisResult &,
-                                           JSONFormat::EntityIdToJSONFn) {
+json::Object serializeFailingDeserializerAnalysisResult(
+    const FailingDeserializerAnalysisResult &, JSONFormat::EntityIdToJSONFn) {
   return json::Object{};
 }
 
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json b/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
deleted file mode 100644
index 2255b4eb1ae38..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/Inputs/lu-empty.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "data": [],
-  "id_table": [],
-  "linkage_table": [],
-  "lu_namespace": [
-    {
-      "kind": "LinkUnit",
-      "name": "test"
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test b/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
deleted file mode 100644
index 4f1b24f34247c..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/cli.test
+++ /dev/null
@@ -1,10 +0,0 @@
-// Tests for clang-ssaf-analyzer command-line option validation.
-
-// RUN: not clang-ssaf-analyzer 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-ARGS
-// NO-ARGS:      clang-ssaf-analyzer{{(\.exe)?}}: error: no LUSummary file specified
-// NO-ARGS-NOT:  {{.+}}
-
-// RUN: not clang-ssaf-analyzer %S/Inputs/lu-empty.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT
-// NO-OUTPUT: clang-ssaf-analyzer{{(\.exe)?}}: for the -o option: must be specified at least once!
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/io.test b/clang/test/Analysis/Scalable/ssaf-analyzer/io.test
deleted file mode 100644
index e5a7d7a06bd91..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/io.test
+++ /dev/null
@@ -1,21 +0,0 @@
-// Tests for clang-ssaf-analyzer file I/O error handling.
-
-// RUN: rm -rf %t
-// RUN: mkdir -p %t
-
-// Nonexistent input file.
-// RUN: not clang-ssaf-analyzer %t/nonexistent.json -o %t/out.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT
-// NO-INPUT: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}nonexistent.json'{{.*}}
-
-// Input path with no extension.
-// RUN: cp %S/Inputs/lu-empty.json %t/lu-noext
-// RUN: not clang-ssaf-analyzer %t/lu-noext -o %t/out.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-EXT
-// NO-EXT: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}lu-noext'{{.*}}
-
-// Output parent directory does not exist.
-// RUN: not clang-ssaf-analyzer %S/Inputs/lu-empty.json \
-// RUN:   -o %t/nosuchdir/out.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT-DIR
-// NO-OUTPUT-DIR: clang-ssaf-analyzer{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.json'{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test b/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
deleted file mode 100644
index 45a7a87efe502..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-analyzer/plugin.test
+++ /dev/null
@@ -1,80 +0,0 @@
-// Tests for clang-ssaf-analyzer WPASuite serialization via the example plugin.
-// The SSAFExamplePlugin registers TagsAnalysisResult and
-// CountsAnalysisResult together with their JSON serializers so that the
-// round-trip through clang-ssaf-analyzer can be verified end-to-end.
-
-// REQUIRES: plugins
-
-// RUN: rm -rf %t
-// RUN: mkdir -p %t
-
-// ============================================================================
-// Run TagsAnalysis only -- verify the output JSON structure.
-// ============================================================================
-
-// RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   --analysis TagsAnalysisResult \
-// RUN:   -o %t/tags.json \
-// RUN:   %S/Inputs/lu-empty.json
-// RUN: FileCheck %s --input-file %t/tags.json --check-prefix=TAGS-OUT
-
-// TAGS-OUT:      "analysis_name": "TagsAnalysisResult"
-// TAGS-OUT:      "tags":
-// TAGS-OUT-DAG:  "alpha"
-// TAGS-OUT-DAG:  "beta"
-// TAGS-OUT-DAG:  "gamma"
-
-// ============================================================================
-// Run CountsAnalysis only -- verify the output JSON structure.
-// ============================================================================
-
-// RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   --analysis CountsAnalysisResult \
-// RUN:   -o %t/counts.json \
-// RUN:   %S/Inputs/lu-empty.json
-// RUN: FileCheck %s --input-file %t/counts.json --check-prefix=COUNTS-OUT
-
-// COUNTS-OUT: "analysis_name": "CountsAnalysisResult"
-// COUNTS-OUT: "counts": []
-
-// ============================================================================
-// Run both analyses -- verify both results appear in the output.
-// ============================================================================
-
-// RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   --analysis TagsAnalysisResult \
-// RUN:   --analysis CountsAnalysisResult \
-// RUN:   -o %t/both.json \
-// RUN:   %S/Inputs/lu-empty.json
-// RUN: FileCheck %s --input-file %t/both.json --check-prefix=BOTH-OUT
-
-// BOTH-OUT-DAG: "analysis_name": "TagsAnalysisResult"
-// BOTH-OUT-DAG: "analysis_name": "CountsAnalysisResult"
-
-// ============================================================================
-// Run all registered analyses (no --analysis flags).
-// ============================================================================
-
-// RUN: clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   -o %t/all.json \
-// RUN:   %S/Inputs/lu-empty.json
-// RUN: FileCheck %s --input-file %t/all.json --check-prefix=ALL-OUT
-
-// ALL-OUT-DAG: "analysis_name": "TagsAnalysisResult"
-// ALL-OUT-DAG: "analysis_name": "CountsAnalysisResult"
-
-// ============================================================================
-// Unknown analysis name produces an error.
-// ============================================================================
-
-// RUN: not clang-ssaf-analyzer \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   --analysis UnknownAnalysis \
-// RUN:   -o %t/unknown.json \
-// RUN:   %S/Inputs/lu-empty.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=UNKNOWN-ANALYSIS
-// UNKNOWN-ANALYSIS: error: {{.*}}UnknownAnalysis{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
index 81b2733e5b882..9ebc349c88f59 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
@@ -11,7 +11,7 @@
 // Nonexistent file.
 // RUN: not clang-ssaf-format --type wpa %t/nonexistent.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-FILE
-// NO-FILE: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}nonexistent.json'{{.*}}
+// NO-FILE: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}nonexistent.json'{{.*}}
 
 // Input path is a directory.
 // RUN: mkdir %t/test_directory.json
@@ -23,7 +23,7 @@
 // RUN: cp %S/Inputs/rt-empty.json %t/test.txt
 // RUN: not clang-ssaf-format --type wpa %t/test.txt 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=BAD-EXT
-// BAD-EXT: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}test.txt'{{.*}}
+// BAD-EXT: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}test.txt'{{.*}}
 
 // ============================================================================
 // Read errors: JSON structure
@@ -114,16 +114,16 @@
 // RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
 // RUN:   -o %t/existing.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=OUT-EXISTS
-// OUT-EXISTS: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}existing.json'{{.*}}Output file already exists
+// OUT-EXISTS: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}existing.json'{{.*}}Output file already exists
 
 // Output parent directory does not exist.
 // RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
 // RUN:   -o %t/nosuchdir/out.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-OUT-DIR
-// NO-OUT-DIR: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.json'{{.*}}
+// NO-OUT-DIR: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}out.json'{{.*}}
 
 // Output path has an unrecognised extension.
 // RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
 // RUN:   -o %t/out.txt 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=BAD-OUT-EXT
-// BAD-OUT-EXT: clang-ssaf-format{{(\.exe)?}}: error: cannot validate summary '{{.*}}out.txt'{{.*}}
+// BAD-OUT-EXT: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}out.txt'{{.*}}

>From 3232d5cf1c7a7676beb5103948a8fbb5546b3a6f Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 16:27:43 -0700
Subject: [PATCH 26/30] More renaming

---
 .../Core/Serialization/SerializationFormat.h  |   3 +-
 .../WholeProgramAnalysis/DerivedAnalysis.h    |   4 +-
 .../Plugins/ExamplePlugin/ExamplePlugin.cpp   |   4 +-
 .../Inputs/duplicate-analysis-name.json       |   0
 .../Inputs/missing-results.json               |   0
 ...result-entry-analysis-name-not-string.json |   0
 .../result-entry-deserializer-error.json      |   0
 .../result-entry-missing-analysis-name.json   |   0
 .../result-entry-missing-result-field.json    |   0
 .../Inputs/result-entry-no-format-info.json   |   0
 .../Inputs/result-entry-not-object.json       |   0
 .../result-entry-result-not-object.json       |   0
 .../Inputs/results-not-array.json             |   0
 .../Inputs/rt-counts-with-entities.json       |  36 ++++
 .../Inputs/rt-empty-payload.json              |   4 +-
 .../Scalable/ssaf-format/Inputs/rt-empty.json |   4 +
 .../Inputs/rt-multiple-results.json           |  24 ++-
 .../ssaf-format/Inputs/rt-single-tags.json    |  15 ++
 .../Scalable/ssaf-format/wpa-suite.test       | 156 ++++++++++++++++++
 .../wpa-suite/Inputs/id-table-not-array.json  |   1 -
 .../wpa-suite/Inputs/malformed.json           |   1 -
 .../wpa-suite/Inputs/missing-id-table.json    |   1 -
 .../wpa-suite/Inputs/not-object.json          |   1 -
 .../Inputs/rt-counts-with-entities.json       |  25 ---
 .../wpa-suite/Inputs/rt-empty.json            |   1 -
 .../wpa-suite/Inputs/rt-single-tags.json      |   9 -
 .../ssaf-format/wpa-suite/errors.test         | 129 ---------------
 .../ssaf-format/wpa-suite/plugin.test         | 120 --------------
 clang/test/CMakeLists.txt                     |   6 +
 29 files changed, 244 insertions(+), 300 deletions(-)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/duplicate-analysis-name.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/missing-results.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-analysis-name-not-string.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-deserializer-error.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-missing-analysis-name.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-missing-result-field.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-no-format-info.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-not-object.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/result-entry-result-not-object.json (100%)
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/results-not-array.json (100%)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/rt-empty-payload.json (68%)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty.json
 rename clang/test/Analysis/Scalable/ssaf-format/{wpa-suite => }/Inputs/rt-multiple-results.json (51%)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test

diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
index b90411f910730..fe71e37f08878 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h
@@ -143,7 +143,8 @@ class SerializationFormat {
         }
         Registered = true;
         SA::Saved = TypedSerialize;
-        static SerializerFn SavedSerialize(&SA::wrap);
+        static auto *SerializeWrap = &SA::wrap;
+        static SerializerFn SavedSerialize(SerializeWrap);
         static DeserializerFn SavedDeserialize = Deserialize;
 
         struct ConcreteEntry : Entry {
diff --git a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
index 4eb35262d4625..0e1eb8e9b8b87 100644
--- a/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
+++ b/clang/include/clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/DerivedAnalysis.h
@@ -99,10 +99,10 @@ class DerivedAnalysis : public DerivedAnalysisBase {
 
   /// Performs one step. Returns true if another step is needed; false when
   /// converged. Single-step analyses always return false.
-  virtual llvm::Expected<bool> step() = 0;
+  virtual llvm::Expected<bool> step() override = 0;
 
   /// Called after the step() loop converges. Override for post-processing.
-  virtual llvm::Error finalize() { return llvm::Error::success(); }
+  virtual llvm::Error finalize() override { return llvm::Error::success(); }
 
 protected:
   /// Read-only access to the result being built.
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
index 59b5b9f37f181..1d474384919e5 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
@@ -77,8 +77,8 @@ deserializeTagsAnalysisResult(const json::Object &Obj,
 }
 
 JSONFormat::AnalysisResultRegistry::Add<TagsAnalysisResult>
-    RegisterTagsForJSON(serializeTagsAnalysisResult,
-                        deserializeTagsAnalysisResult);
+    RegisterJSONFormatSupportForTagAnalysisResult(
+        serializeTagsAnalysisResult, deserializeTagsAnalysisResult);
 
 //===----------------------------------------------------------------------===//
 // TagsAnalysis
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/duplicate-analysis-name.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/duplicate-analysis-name.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/duplicate-analysis-name.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/missing-results.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-results.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/missing-results.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-analysis-name-not-string.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-analysis-name-not-string.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-analysis-name-not-string.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-deserializer-error.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-missing-analysis-name.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-analysis-name.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-missing-analysis-name.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-missing-result-field.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-missing-result-field.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-missing-result-field.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-no-format-info.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-no-format-info.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-no-format-info.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-not-object.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-not-object.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-not-object.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-result-not-object.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/result-entry-result-not-object.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-result-not-object.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/results-not-array.json
similarity index 100%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/results-not-array.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/results-not-array.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
new file mode 100644
index 0000000000000..d1cad93f69b92
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
@@ -0,0 +1,36 @@
+{
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "a.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "a.exe"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at foo"
+      }
+    }
+  ],
+  "results": [
+    {
+      "analysis_name": "CountsAnalysisResult",
+      "result": {
+        "counts": [
+          {
+            "count": 42,
+            "entity_id": {
+              "@": 0
+            }
+          }
+        ]
+      }
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
similarity index 68%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
index 590b413e530c3..8bbe45728a9ce 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty-payload.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
@@ -3,7 +3,9 @@
   "results": [
     {
       "analysis_name": "TagsAnalysisResult",
-      "result": {"tags": []}
+      "result": {
+        "tags": []
+      }
     }
   ]
 }
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty.json
new file mode 100644
index 0000000000000..80637e10fc66f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty.json
@@ -0,0 +1,4 @@
+{
+  "id_table": [],
+  "results": []
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-multiple-results.json
similarity index 51%
rename from clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json
rename to clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-multiple-results.json
index 5098e0eaa787a..764d2dbd2e83a 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-multiple-results.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-multiple-results.json
@@ -3,11 +3,14 @@
     {
       "id": 0,
       "name": {
-        "usr": "c:@F at foo",
-        "suffix": "",
         "namespace": [
-          {"kind": "LinkUnit", "name": "test.exe"}
-        ]
+          {
+            "kind": "LinkUnit",
+            "name": "test.exe"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at foo"
       }
     }
   ],
@@ -16,13 +19,22 @@
       "analysis_name": "CountsAnalysisResult",
       "result": {
         "counts": [
-          {"entity_id": {"@": 0}, "count": 100}
+          {
+            "count": 100,
+            "entity_id": {
+              "@": 0
+            }
+          }
         ]
       }
     },
     {
       "analysis_name": "TagsAnalysisResult",
-      "result": {"tags": ["important"]}
+      "result": {
+        "tags": [
+          "important"
+        ]
+      }
     }
   ]
 }
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
new file mode 100644
index 0000000000000..9706488c34f60
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
@@ -0,0 +1,15 @@
+{
+  "id_table": [],
+  "results": [
+    {
+      "analysis_name": "TagsAnalysisResult",
+      "result": {
+        "tags": [
+          "alpha",
+          "beta",
+          "gamma"
+        ]
+      }
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
new file mode 100644
index 0000000000000..bb5e239659f68
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
@@ -0,0 +1,156 @@
+// Tests for clang-ssaf-format --type wpa that require the example plugin.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Read errors: results field
+// ============================================================================
+
+// 'results' field is absent.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/missing-results.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-RESULTS
+// NO-RESULTS:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}missing-results.json'
+// NO-RESULTS-NEXT: failed to read WPA results from field 'results': expected JSON array
+
+// 'results' is an object, not an array.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/results-not-array.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=RESULTS-OBJ
+// RESULTS-OBJ:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}results-not-array.json'
+// RESULTS-OBJ-NEXT: failed to read WPA results from field 'results': expected JSON array
+
+// A result entry is a string, not an object.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/result-entry-not-object.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ENTRY-NOT-OBJ
+// ENTRY-NOT-OBJ:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}result-entry-not-object.json'
+// ENTRY-NOT-OBJ-NEXT: reading WPA results from field 'results'
+// ENTRY-NOT-OBJ-NEXT: failed to read WPA result entry from index '0': expected JSON object
+
+// 'analysis_name' field is absent.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-missing-analysis-name.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-NAME
+// NO-NAME:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// NO-NAME:      reading WPA result entry from index '0'
+// NO-NAME-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
+
+// 'analysis_name' is an integer, not a string.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-analysis-name-not-string.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NAME-NOT-STR
+// NAME-NOT-STR:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// NAME-NOT-STR-NEXT: reading WPA results from field 'results'
+// NAME-NOT-STR-NEXT: reading WPA result entry from index '0'
+// NAME-NOT-STR-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
+
+// Analysis name is not registered (no plugin loaded).
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   %S/Inputs/result-entry-no-format-info.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-FORMAT-INFO
+// NO-FORMAT-INFO:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// NO-FORMAT-INFO:      reading WPA result entry from index '0'
+// NO-FORMAT-INFO-NEXT: no support registered for analysis: AnalysisName(UnregisteredAnalysis)
+
+// ============================================================================
+// Write errors
+// ============================================================================
+
+// Output file already exists.
+// RUN: touch %t/existing.json
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/existing.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUT-EXISTS
+// OUT-EXISTS: clang-ssaf-format: error: failed to validate summary '{{.*}}existing.json'{{.*}}Output file already exists
+
+// Output parent directory does not exist.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/nosuchdir/out.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-OUT-DIR
+// NO-OUT-DIR: clang-ssaf-format: error: failed to validate summary '{{.*}}out.json'{{.*}}
+
+// Output path has an unrecognised extension.
+// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
+// RUN:   -o %t/out.txt 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-OUT-EXT
+// BAD-OUT-EXT: clang-ssaf-format: error: failed to validate summary '{{.*}}out.txt'{{.*}}
+
+// ============================================================================
+// Read errors requiring a registered deserializer
+// ============================================================================
+
+// 'result' field is absent; the deserializer lookup succeeds but the field
+// check fires before the deserializer is called.
+// RUN: not clang-ssaf-format --type wpa --load %llvmshlibdir/SSAFExamplePlugin%pluginext %S/Inputs/result-entry-missing-result-field.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-RESULT-FIELD
+// NO-RESULT-FIELD:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// NO-RESULT-FIELD-NEXT: reading WPA results from field 'results'
+// NO-RESULT-FIELD-NEXT: reading WPA result entry from index '0'
+// NO-RESULT-FIELD-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
+
+// 'result' is a string, not an object.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/result-entry-result-not-object.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=RESULT-NOT-OBJ
+// RESULT-NOT-OBJ:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// RESULT-NOT-OBJ-NEXT: reading WPA results from field 'results'
+// RESULT-NOT-OBJ-NEXT: reading WPA result entry from index '0'
+// RESULT-NOT-OBJ-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
+
+// Deserializer returns an error.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/result-entry-deserializer-error.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DESER-ERR
+// DESER-ERR:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// DESER-ERR-NEXT: reading WPA results from field 'results'
+// DESER-ERR-NEXT: reading WPA result entry from index '0'
+// DESER-ERR-NEXT: intentional deserializer failure
+
+// Duplicate analysis name in results array.
+// RUN: not clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/duplicate-analysis-name.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUPLICATE
+// DUPLICATE:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
+// DUPLICATE-NEXT: reading WPA results from field 'results'
+// DUPLICATE-NEXT: failed to insert WPA result at index '1': encountered duplicate 'AnalysisName(TagsAnalysisResult)'
+
+// ============================================================================
+// Round-trip tests
+//
+// Input files are pre-normalized to match the tool's output format
+// (alphabetical keys, expanded formatting), so a plain diff suffices.
+// ============================================================================
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/rt-empty.json \
+// RUN:   -o %t/rt-empty.json
+// RUN: diff %S/Inputs/rt-empty.json %t/rt-empty.json
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/rt-single-tags.json \
+// RUN:   -o %t/rt-single-tags.json
+// RUN: diff %S/Inputs/rt-single-tags.json %t/rt-single-tags.json
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/rt-empty-payload.json \
+// RUN:   -o %t/rt-empty-payload.json
+// RUN: diff %S/Inputs/rt-empty-payload.json %t/rt-empty-payload.json
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/rt-counts-with-entities.json \
+// RUN:   -o %t/rt-counts-with-entities.json
+// RUN: diff %S/Inputs/rt-counts-with-entities.json %t/rt-counts-with-entities.json
+
+// RUN: clang-ssaf-format --type wpa \
+// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
+// RUN:   %S/Inputs/rt-multiple-results.json \
+// RUN:   -o %t/rt-multiple-results.json
+// RUN: diff %S/Inputs/rt-multiple-results.json %t/rt-multiple-results.json
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
deleted file mode 100644
index 8754744da6aae..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/id-table-not-array.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id_table": {}, "results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
deleted file mode 100644
index b0e13f61aa06e..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/malformed.json
+++ /dev/null
@@ -1 +0,0 @@
-{ invalid json }
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
deleted file mode 100644
index 330e8fe3131fd..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/missing-id-table.json
+++ /dev/null
@@ -1 +0,0 @@
-{"results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
deleted file mode 100644
index fe51488c7066f..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/not-object.json
+++ /dev/null
@@ -1 +0,0 @@
-[]
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
deleted file mode 100644
index f5bc930231606..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-counts-with-entities.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
-  "id_table": [
-    {
-      "id": 0,
-      "name": {
-        "usr": "c:@F at foo",
-        "suffix": "",
-        "namespace": [
-          {"kind": "CompilationUnit", "name": "a.cpp"},
-          {"kind": "LinkUnit", "name": "a.exe"}
-        ]
-      }
-    }
-  ],
-  "results": [
-    {
-      "analysis_name": "CountsAnalysisResult",
-      "result": {
-        "counts": [
-          {"entity_id": {"@": 0}, "count": 42}
-        ]
-      }
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
deleted file mode 100644
index 761ca67c45e9f..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-empty.json
+++ /dev/null
@@ -1 +0,0 @@
-{"id_table": [], "results": []}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
deleted file mode 100644
index fcd010f124407..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/Inputs/rt-single-tags.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "id_table": [],
-  "results": [
-    {
-      "analysis_name": "TagsAnalysisResult",
-      "result": {"tags": ["alpha", "beta", "gamma"]}
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
deleted file mode 100644
index 9ebc349c88f59..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/errors.test
+++ /dev/null
@@ -1,129 +0,0 @@
-// Tests for clang-ssaf-format --type wpa error handling.
-// None of these tests require a plugin.
-
-// RUN: rm -rf %t
-// RUN: mkdir -p %t
-
-// ============================================================================
-// Read errors: file system
-// ============================================================================
-
-// Nonexistent file.
-// RUN: not clang-ssaf-format --type wpa %t/nonexistent.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-FILE
-// NO-FILE: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}nonexistent.json'{{.*}}
-
-// Input path is a directory.
-// RUN: mkdir %t/test_directory.json
-// RUN: not clang-ssaf-format --type wpa %t/test_directory.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=IS-DIR
-// IS-DIR: clang-ssaf-format{{(\.exe)?}}: error: {{.*}}test_directory.json{{.*}}
-
-// Input has an unrecognised extension (no format registered for '.txt').
-// RUN: cp %S/Inputs/rt-empty.json %t/test.txt
-// RUN: not clang-ssaf-format --type wpa %t/test.txt 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-EXT
-// BAD-EXT: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}test.txt'{{.*}}
-
-// ============================================================================
-// Read errors: JSON structure
-// ============================================================================
-
-// Invalid JSON syntax.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/malformed.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-JSON
-// BAD-JSON:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}malformed.json'
-// BAD-JSON-NEXT: {{.*}}Expected object key{{.*}}
-
-// Top-level value is not a JSON object.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/not-object.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NOT-OBJ
-// NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}not-object.json'
-// NOT-OBJ-NEXT: failed to read WPASuite: expected JSON object
-
-// ============================================================================
-// Read errors: id_table field
-// ============================================================================
-
-// 'id_table' field is absent.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/missing-id-table.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-ID-TABLE
-// NO-ID-TABLE:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}missing-id-table.json'
-// NO-ID-TABLE-NEXT: failed to read IdTable from field 'id_table': expected JSON array
-
-// 'id_table' is an object, not an array.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/id-table-not-array.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=ID-TABLE-OBJ
-// ID-TABLE-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}id-table-not-array.json'
-// ID-TABLE-OBJ-NEXT: failed to read IdTable from field 'id_table': expected JSON array
-
-// ============================================================================
-// Read errors: results field
-// ============================================================================
-
-// 'results' field is absent.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/missing-results.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-RESULTS
-// NO-RESULTS:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}missing-results.json'
-// NO-RESULTS-NEXT: failed to read WPA results from field 'results': expected JSON array
-
-// 'results' is an object, not an array.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/results-not-array.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=RESULTS-OBJ
-// RESULTS-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}results-not-array.json'
-// RESULTS-OBJ-NEXT: failed to read WPA results from field 'results': expected JSON array
-
-// A result entry is a string, not an object.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/result-entry-not-object.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=ENTRY-NOT-OBJ
-// ENTRY-NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}result-entry-not-object.json'
-// ENTRY-NOT-OBJ-NEXT: reading WPA results from field 'results'
-// ENTRY-NOT-OBJ-NEXT: failed to read WPA result entry from index '0': expected JSON object
-
-// 'analysis_name' field is absent.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   %S/Inputs/result-entry-missing-analysis-name.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-NAME
-// NO-NAME:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// NO-NAME:      reading WPA result entry from index '0'
-// NO-NAME-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
-
-// 'analysis_name' is an integer, not a string.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   %S/Inputs/result-entry-analysis-name-not-string.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NAME-NOT-STR
-// NAME-NOT-STR:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// NAME-NOT-STR-NEXT: reading WPA results from field 'results'
-// NAME-NOT-STR-NEXT: reading WPA result entry from index '0'
-// NAME-NOT-STR-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
-
-// Analysis name is not registered (no plugin loaded).
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   %S/Inputs/result-entry-no-format-info.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-FORMAT-INFO
-// NO-FORMAT-INFO:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// NO-FORMAT-INFO:      reading WPA result entry from index '0'
-// NO-FORMAT-INFO-NEXT: no support registered for analysis: AnalysisName(UnregisteredAnalysis)
-
-// ============================================================================
-// Write errors
-// ============================================================================
-
-// Output file already exists.
-// RUN: touch %t/existing.json
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/existing.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=OUT-EXISTS
-// OUT-EXISTS: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}existing.json'{{.*}}Output file already exists
-
-// Output parent directory does not exist.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/nosuchdir/out.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-OUT-DIR
-// NO-OUT-DIR: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}out.json'{{.*}}
-
-// Output path has an unrecognised extension.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/out.txt 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-OUT-EXT
-// BAD-OUT-EXT: clang-ssaf-format{{(\.exe)?}}: error: failed to validate summary '{{.*}}out.txt'{{.*}}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
deleted file mode 100644
index a80baf1206fbf..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite/plugin.test
+++ /dev/null
@@ -1,120 +0,0 @@
-// Tests for clang-ssaf-format --type wpa that require the example plugin. Covers:
-//   - Read errors that need a registered deserializer to reach
-//   - Round-trip correctness (read → write → verify)
-//   - Content verification (field values in the output)
-
-// REQUIRES: plugins
-
-// RUN: rm -rf %t
-// RUN: mkdir -p %t
-
-// ============================================================================
-// Read errors requiring a registered deserializer
-// ============================================================================
-
-// 'result' field is absent; the deserializer lookup succeeds but the field
-// check fires before the deserializer is called.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/result-entry-missing-result-field.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-RESULT-FIELD
-// NO-RESULT-FIELD:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// NO-RESULT-FIELD:      reading WPA result entry from index '0'
-// NO-RESULT-FIELD-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
-
-// 'result' is a string, not an object.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/result-entry-result-not-object.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=RESULT-NOT-OBJ
-// RESULT-NOT-OBJ:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// RESULT-NOT-OBJ-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
-
-// Deserializer returns an error.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/result-entry-deserializer-error.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=DESER-ERR
-// DESER-ERR:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// DESER-ERR:      reading WPA result entry from index '0'
-// DESER-ERR-NEXT: intentional deserializer failure
-
-// Duplicate analysis name in results array.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/duplicate-analysis-name.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=DUPLICATE
-// DUPLICATE:      clang-ssaf-format{{(\.exe)?}}: error: reading WPASuite from file '{{.*}}'
-// DUPLICATE:      reading WPA results from field 'results'
-// DUPLICATE-NEXT: failed to insert WPA result at index '1': encountered duplicate 'AnalysisName(TagsAnalysisResult)'
-
-// ============================================================================
-// Round-trip: RoundTripEmpty
-// ============================================================================
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-empty.json \
-// RUN:   -o %t/rt-empty.json
-// RUN: FileCheck %s --input-file %t/rt-empty.json --check-prefix=RT-EMPTY
-// RT-EMPTY: "id_table": []
-// RT-EMPTY: "results": []
-
-// ============================================================================
-// Round-trip + content verification: RoundTripSingleResultNoEntities /
-//                                    ReadVerifyTagsResult
-// ============================================================================
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-single-tags.json \
-// RUN:   -o %t/rt-single-tags.json
-// RUN: FileCheck %s --input-file %t/rt-single-tags.json --check-prefix=RT-TAGS
-// RT-TAGS:     "analysis_name": "TagsAnalysisResult"
-// RT-TAGS:     "tags":
-// RT-TAGS-DAG: "alpha"
-// RT-TAGS-DAG: "beta"
-// RT-TAGS-DAG: "gamma"
-
-// ============================================================================
-// Round-trip + content verification: RoundTripEmptyResultPayload
-// ============================================================================
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-empty-payload.json \
-// RUN:   -o %t/rt-empty-payload.json
-// RUN: FileCheck %s --input-file %t/rt-empty-payload.json --check-prefix=RT-EMPTY-PAYLOAD
-// RT-EMPTY-PAYLOAD: "analysis_name": "TagsAnalysisResult"
-// RT-EMPTY-PAYLOAD: "tags": []
-
-// ============================================================================
-// Round-trip + content verification: RoundTripSingleResultWithEntityRefs /
-//                                    ReadVerifyCountsResultWithEntityId
-// ============================================================================
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-counts-with-entities.json \
-// RUN:   -o %t/rt-counts-with-entities.json
-// RUN: FileCheck %s --input-file %t/rt-counts-with-entities.json --check-prefix=RT-COUNTS
-// RT-COUNTS:     "analysis_name": "CountsAnalysisResult"
-// RT-COUNTS:     "counts":
-// RT-COUNTS:     "entity_id":
-// RT-COUNTS:     "@":
-// RT-COUNTS:     "count": 42
-// The entity appears in the id_table with its original name.
-// RT-COUNTS:     "usr": "c:@F at foo"
-
-// ============================================================================
-// Round-trip: RoundTripMultipleResults
-// ============================================================================
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-multiple-results.json \
-// RUN:   -o %t/rt-multiple-results.json
-// RUN: FileCheck %s --input-file %t/rt-multiple-results.json --check-prefix=RT-MULTI
-// RT-MULTI-DAG: "analysis_name": "CountsAnalysisResult"
-// RT-MULTI-DAG: "analysis_name": "TagsAnalysisResult"
-// RT-MULTI:     "important"
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index db12d4ee38fe4..0f046b24c2747 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -207,6 +207,12 @@ if(CLANG_ENABLE_STATIC_ANALYZER)
   endif()
 endif()
 
+if(LLVM_ENABLE_PLUGINS)
+  list(APPEND CLANG_TEST_DEPS
+    SSAFExamplePlugin
+    )
+endif()
+
 if (HAVE_CLANG_REPL_SUPPORT)
   list(APPEND CLANG_TEST_DEPS
     clang-repl

>From 0d290502ba32b5a993692fea044d3616f2d194cb Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 16:50:06 -0700
Subject: [PATCH 27/30] Tree shaking

---
 .../Plugins/ExamplePlugin/ExamplePlugin.cpp   | 38 +--------
 .../result-entry-deserializer-error.json      |  2 +-
 .../Inputs/rt-counts-with-entities.json       | 36 --------
 .../ssaf-format/Inputs/rt-empty-payload.json  | 11 ---
 .../ssaf-format/Inputs/rt-single-tags.json    | 15 ----
 .../Scalable/ssaf-format/lit.local.cfg        |  5 ++
 .../Scalable/ssaf-format/wpa-suite.test       | 84 +++----------------
 7 files changed, 22 insertions(+), 169 deletions(-)
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
index 1d474384919e5..d19ddb4d72598 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
@@ -6,10 +6,9 @@
 //
 //===----------------------------------------------------------------------===//
 //
-// A loadable plugin that registers three analysis result types — Tags, Counts,
-// and FailingDeserializer — together with their JSON serializers/deserializers
-// and trivial DerivedAnalysis implementations. Used by lit tests for both
-// clang-ssaf-analyzer and clang-ssaf-format.
+// A loadable plugin that registers two analysis result types — Tags and
+// Counts — together with their JSON serializers/deserializers and trivial
+// DerivedAnalysis implementations. Used by lit tests for clang-ssaf-format.
 //
 //===----------------------------------------------------------------------===//
 
@@ -189,35 +188,4 @@ class CountsAnalysis final : public DerivedAnalysis<CountsAnalysisResult> {
 AnalysisRegistry::Add<CountsAnalysis> RegisterCountsAnalysis(
     "Produces an empty counts result for testing WPASuite serialization");
 
-//===----------------------------------------------------------------------===//
-// FailingDeserializerAnalysisResult
-//
-// An analysis result whose deserializer always returns an error. Used to
-// exercise error propagation in the WPASuite read path without needing a
-// malformed payload.
-//===----------------------------------------------------------------------===//
-
-struct FailingDeserializerAnalysisResult final : AnalysisResult {
-  static AnalysisName analysisName() {
-    return AnalysisName("FailingDeserializerAnalysisResult");
-  }
-};
-
-json::Object serializeFailingDeserializerAnalysisResult(
-    const FailingDeserializerAnalysisResult &, JSONFormat::EntityIdToJSONFn) {
-  return json::Object{};
-}
-
-Expected<std::unique_ptr<AnalysisResult>>
-deserializeFailingDeserializerAnalysisResult(const json::Object &,
-                                             JSONFormat::EntityIdFromJSONFn) {
-  return createStringError(inconvertibleErrorCode(),
-                           "intentional deserializer failure");
-}
-
-JSONFormat::AnalysisResultRegistry::Add<FailingDeserializerAnalysisResult>
-    RegisterFailingDeserializerForJSON(
-        serializeFailingDeserializerAnalysisResult,
-        deserializeFailingDeserializerAnalysisResult);
-
 } // namespace
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json
index 7f1200e06201f..c99b1a9e534af 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json
+++ b/clang/test/Analysis/Scalable/ssaf-format/Inputs/result-entry-deserializer-error.json
@@ -1 +1 @@
-{"id_table": [], "results": [{"analysis_name": "FailingDeserializerAnalysisResult", "result": {}}]}
+{"id_table": [], "results": [{"analysis_name": "TagsAnalysisResult", "result": {"tags": "not-an-array"}}]}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
deleted file mode 100644
index d1cad93f69b92..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-counts-with-entities.json
+++ /dev/null
@@ -1,36 +0,0 @@
-{
-  "id_table": [
-    {
-      "id": 0,
-      "name": {
-        "namespace": [
-          {
-            "kind": "CompilationUnit",
-            "name": "a.cpp"
-          },
-          {
-            "kind": "LinkUnit",
-            "name": "a.exe"
-          }
-        ],
-        "suffix": "",
-        "usr": "c:@F at foo"
-      }
-    }
-  ],
-  "results": [
-    {
-      "analysis_name": "CountsAnalysisResult",
-      "result": {
-        "counts": [
-          {
-            "count": 42,
-            "entity_id": {
-              "@": 0
-            }
-          }
-        ]
-      }
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
deleted file mode 100644
index 8bbe45728a9ce..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-empty-payload.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
-  "id_table": [],
-  "results": [
-    {
-      "analysis_name": "TagsAnalysisResult",
-      "result": {
-        "tags": []
-      }
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json b/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
deleted file mode 100644
index 9706488c34f60..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-format/Inputs/rt-single-tags.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-  "id_table": [],
-  "results": [
-    {
-      "analysis_name": "TagsAnalysisResult",
-      "result": {
-        "tags": [
-          "alpha",
-          "beta",
-          "gamma"
-        ]
-      }
-    }
-  ]
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg b/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg
new file mode 100644
index 0000000000000..2c55e9eda0e2e
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg
@@ -0,0 +1,5 @@
+config.substitutions.insert(0,
+    ("%ssaf-format-with-plugin",
+     "clang-ssaf-format --type wpa"
+     " --load {}/SSAFExamplePlugin{}".format(
+         config.llvm_shlib_dir, config.llvm_plugin_ext)))
diff --git a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
index bb5e239659f68..9964ffbcb8e63 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
+++ b/clang/test/Analysis/Scalable/ssaf-format/wpa-suite.test
@@ -6,7 +6,7 @@
 // RUN: mkdir -p %t
 
 // ============================================================================
-// Read errors: results field
+// Read errors
 // ============================================================================
 
 // 'results' field is absent.
@@ -33,7 +33,8 @@
 // RUN:   %S/Inputs/result-entry-missing-analysis-name.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-NAME
 // NO-NAME:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
-// NO-NAME:      reading WPA result entry from index '0'
+// NO-NAME-NEXT: reading WPA results from field 'results'
+// NO-NAME-NEXT: reading WPA result entry from index '0'
 // NO-NAME-NEXT: failed to read AnalysisName from field 'analysis_name': expected JSON string
 
 // 'analysis_name' is an integer, not a string.
@@ -50,39 +51,13 @@
 // RUN:   %S/Inputs/result-entry-no-format-info.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-FORMAT-INFO
 // NO-FORMAT-INFO:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
-// NO-FORMAT-INFO:      reading WPA result entry from index '0'
+// NO-FORMAT-INFO-NEXT: reading WPA results from field 'results'
+// NO-FORMAT-INFO-NEXT: reading WPA result entry from index '0'
 // NO-FORMAT-INFO-NEXT: no support registered for analysis: AnalysisName(UnregisteredAnalysis)
 
-// ============================================================================
-// Write errors
-// ============================================================================
-
-// Output file already exists.
-// RUN: touch %t/existing.json
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/existing.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=OUT-EXISTS
-// OUT-EXISTS: clang-ssaf-format: error: failed to validate summary '{{.*}}existing.json'{{.*}}Output file already exists
-
-// Output parent directory does not exist.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/nosuchdir/out.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-OUT-DIR
-// NO-OUT-DIR: clang-ssaf-format: error: failed to validate summary '{{.*}}out.json'{{.*}}
-
-// Output path has an unrecognised extension.
-// RUN: not clang-ssaf-format --type wpa %S/Inputs/rt-empty.json \
-// RUN:   -o %t/out.txt 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-OUT-EXT
-// BAD-OUT-EXT: clang-ssaf-format: error: failed to validate summary '{{.*}}out.txt'{{.*}}
-
-// ============================================================================
-// Read errors requiring a registered deserializer
-// ============================================================================
-
 // 'result' field is absent; the deserializer lookup succeeds but the field
 // check fires before the deserializer is called.
-// RUN: not clang-ssaf-format --type wpa --load %llvmshlibdir/SSAFExamplePlugin%pluginext %S/Inputs/result-entry-missing-result-field.json 2>&1 \
+// RUN: not %ssaf-format-with-plugin %S/Inputs/result-entry-missing-result-field.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-RESULT-FIELD
 // NO-RESULT-FIELD:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
 // NO-RESULT-FIELD-NEXT: reading WPA results from field 'results'
@@ -90,29 +65,23 @@
 // NO-RESULT-FIELD-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
 
 // 'result' is a string, not an object.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/result-entry-result-not-object.json 2>&1 \
+// RUN: not %ssaf-format-with-plugin %S/Inputs/result-entry-result-not-object.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=RESULT-NOT-OBJ
 // RESULT-NOT-OBJ:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
 // RESULT-NOT-OBJ-NEXT: reading WPA results from field 'results'
 // RESULT-NOT-OBJ-NEXT: reading WPA result entry from index '0'
 // RESULT-NOT-OBJ-NEXT: failed to read AnalysisResult from field 'result': expected JSON object
 
-// Deserializer returns an error.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/result-entry-deserializer-error.json 2>&1 \
+// Deserializer returns an error (tags field has wrong type).
+// RUN: not %ssaf-format-with-plugin %S/Inputs/result-entry-deserializer-error.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=DESER-ERR
 // DESER-ERR:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
 // DESER-ERR-NEXT: reading WPA results from field 'results'
 // DESER-ERR-NEXT: reading WPA result entry from index '0'
-// DESER-ERR-NEXT: intentional deserializer failure
+// DESER-ERR-NEXT: missing or invalid field 'tags'
 
 // Duplicate analysis name in results array.
-// RUN: not clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/duplicate-analysis-name.json 2>&1 \
+// RUN: not %ssaf-format-with-plugin %S/Inputs/duplicate-analysis-name.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=DUPLICATE
 // DUPLICATE:      clang-ssaf-format: error: reading WPASuite from file '{{.*}}'
 // DUPLICATE-NEXT: reading WPA results from field 'results'
@@ -120,37 +89,10 @@
 
 // ============================================================================
 // Round-trip tests
-//
-// Input files are pre-normalized to match the tool's output format
-// (alphabetical keys, expanded formatting), so a plain diff suffices.
 // ============================================================================
 
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-empty.json \
-// RUN:   -o %t/rt-empty.json
+// RUN: %ssaf-format-with-plugin %S/Inputs/rt-empty.json -o %t/rt-empty.json
 // RUN: diff %S/Inputs/rt-empty.json %t/rt-empty.json
 
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-single-tags.json \
-// RUN:   -o %t/rt-single-tags.json
-// RUN: diff %S/Inputs/rt-single-tags.json %t/rt-single-tags.json
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-empty-payload.json \
-// RUN:   -o %t/rt-empty-payload.json
-// RUN: diff %S/Inputs/rt-empty-payload.json %t/rt-empty-payload.json
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-counts-with-entities.json \
-// RUN:   -o %t/rt-counts-with-entities.json
-// RUN: diff %S/Inputs/rt-counts-with-entities.json %t/rt-counts-with-entities.json
-
-// RUN: clang-ssaf-format --type wpa \
-// RUN:   --load %llvmshlibdir/SSAFExamplePlugin%pluginext \
-// RUN:   %S/Inputs/rt-multiple-results.json \
-// RUN:   -o %t/rt-multiple-results.json
+// RUN: %ssaf-format-with-plugin %S/Inputs/rt-multiple-results.json -o %t/rt-multiple-results.json
 // RUN: diff %S/Inputs/rt-multiple-results.json %t/rt-multiple-results.json

>From d89a8236982e92b03a469c72738049aaa8844b33 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 16:57:31 -0700
Subject: [PATCH 28/30] More fix

---
 .../Plugins/ExamplePlugin/ExamplePlugin.cpp   | 40 -------------------
 1 file changed, 40 deletions(-)

diff --git a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
index d19ddb4d72598..af94db1123360 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
+++ b/clang/lib/ScalableStaticAnalysisFramework/Plugins/ExamplePlugin/ExamplePlugin.cpp
@@ -79,26 +79,6 @@ JSONFormat::AnalysisResultRegistry::Add<TagsAnalysisResult>
     RegisterJSONFormatSupportForTagAnalysisResult(
         serializeTagsAnalysisResult, deserializeTagsAnalysisResult);
 
-//===----------------------------------------------------------------------===//
-// TagsAnalysis
-//
-// A trivial DerivedAnalysis that produces a fixed set of tags, independent
-// of the input LUSummary. Used so that lit tests have predictable output.
-//===----------------------------------------------------------------------===//
-
-class TagsAnalysis final : public DerivedAnalysis<TagsAnalysisResult> {
-public:
-  llvm::Error initialize() override { return llvm::Error::success(); }
-
-  llvm::Expected<bool> step() override {
-    result().Tags = {"alpha", "beta", "gamma"};
-    return false; // converged after one step
-  }
-};
-
-AnalysisRegistry::Add<TagsAnalysis> RegisterTagsAnalysis(
-    "Produces a fixed list of string tags for testing WPASuite serialization");
-
 //===----------------------------------------------------------------------===//
 // CountsAnalysisResult
 //
@@ -168,24 +148,4 @@ JSONFormat::AnalysisResultRegistry::Add<CountsAnalysisResult>
     RegisterCountsForJSON(serializeCountsAnalysisResult,
                           deserializeCountsAnalysisResult);
 
-//===----------------------------------------------------------------------===//
-// CountsAnalysis
-//
-// A trivial DerivedAnalysis that produces an empty counts result. Entity ID
-// serialization is exercised by round-trip tests that supply pre-built JSON.
-//===----------------------------------------------------------------------===//
-
-class CountsAnalysis final : public DerivedAnalysis<CountsAnalysisResult> {
-public:
-  llvm::Error initialize() override { return llvm::Error::success(); }
-
-  llvm::Expected<bool> step() override {
-    // Produces no counts; entity-ID round-trip is covered by direct JSON tests.
-    return false;
-  }
-};
-
-AnalysisRegistry::Add<CountsAnalysis> RegisterCountsAnalysis(
-    "Produces an empty counts result for testing WPASuite serialization");
-
 } // namespace

>From 571fc0f6ab47a9c06c17e6b02d7d5a5d417eea84 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 25 Mar 2026 17:00:46 -0700
Subject: [PATCH 29/30] More

---
 clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg b/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg
index 2c55e9eda0e2e..455e6c58464cb 100644
--- a/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg
+++ b/clang/test/Analysis/Scalable/ssaf-format/lit.local.cfg
@@ -1,5 +1,4 @@
 config.substitutions.insert(0,
     ("%ssaf-format-with-plugin",
-     "clang-ssaf-format --type wpa"
-     " --load {}/SSAFExamplePlugin{}".format(
-         config.llvm_shlib_dir, config.llvm_plugin_ext)))
+     "clang-ssaf-format --type wpa --load {}/SSAFExamplePlugin{}".format(
+              config.llvm_shlib_dir, config.llvm_plugin_ext)))

>From 7fdcbf27172fc71d266e4bcf1ed7e019b3aa3ee5 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 26 Mar 2026 07:34:50 -0700
Subject: [PATCH 30/30] Symbols need exporting on linux

---
 clang/tools/clang-ssaf-format/CMakeLists.txt | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/clang/tools/clang-ssaf-format/CMakeLists.txt b/clang/tools/clang-ssaf-format/CMakeLists.txt
index 864fe5bc27aaa..24bc47b3997e8 100644
--- a/clang/tools/clang-ssaf-format/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-format/CMakeLists.txt
@@ -13,3 +13,5 @@ clang_target_link_libraries(clang-ssaf-format
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkTool
   )
+
+export_executable_symbols_for_plugins(clang-ssaf-format)



More information about the cfe-commits mailing list