[clang] [clang][ssaf] Implement Entity Linker CLI and patching for JSON Format (PR #184713)

Aviral Goel via cfe-commits cfe-commits at lists.llvm.org
Sat Mar 7 22:29:20 PST 2026


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

>From 99acae41e7ea2e46aa44fd55470acb6969cc888d Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Wed, 4 Mar 2026 16:38:03 -0800
Subject: [PATCH 01/13] Patching + EntityLinker CLI

---
 .../EntityLinker/EntitySummaryEncoding.h      |   3 +-
 .../Scalable/Serialization/JSONFormat.h       |  38 +-
 .../Serialization/SerializationFormat.h       |   2 +-
 clang/lib/Analysis/Scalable/CMakeLists.txt    |   1 +
 .../Scalable/EntityLinker/EntityLinker.cpp    |   8 +-
 .../JSONFormat/JSONEntitySummaryEncoding.cpp  |  68 ++
 .../JSONFormat/JSONEntitySummaryEncoding.h    |  55 ++
 .../JSONFormat/JSONFormatImpl.cpp             |  62 +-
 .../Serialization/JSONFormat/JSONFormatImpl.h |  31 +-
 .../Scalable/ssaf-linker/Inputs/tu-1.json     | 159 +++++
 .../Scalable/ssaf-linker/Inputs/tu-2.json     | 160 +++++
 .../Scalable/ssaf-linker/Inputs/tu-badext.txt |   1 +
 .../ssaf-linker/Inputs/tu-dup-id.json         |  28 +
 .../ssaf-linker/Inputs/tu-dup-namespace.json  |   9 +
 .../Scalable/ssaf-linker/Inputs/tu-empty.json |   9 +
 .../ssaf-linker/Inputs/tu-malformed.json      |   1 +
 .../Scalable/ssaf-linker/Inputs/tu-noext      |   1 +
 .../ssaf-linker/Inputs/tu-orphan-data.json    |  30 +
 .../Scalable/ssaf-linker/Outputs/lu-1+2.json  | 652 ++++++++++++++++++
 .../Scalable/ssaf-linker/Outputs/lu-1.json    | 364 ++++++++++
 .../Scalable/ssaf-linker/Outputs/lu-2.json    | 370 ++++++++++
 .../ssaf-linker/Outputs/lu-empty.json         |  11 +
 .../Analysis/Scalable/ssaf-linker/cli.test    |  13 +
 .../Analysis/Scalable/ssaf-linker/help.test   |  23 +
 .../Analysis/Scalable/ssaf-linker/io.test     |  26 +
 .../Scalable/ssaf-linker/linking-errors.test  |   9 +
 .../Scalable/ssaf-linker/linking.test         |  30 +
 .../Analysis/Scalable/ssaf-linker/time.test   |  32 +
 .../ssaf-linker/validation-errors.test        |  49 ++
 .../Scalable/ssaf-linker/verbose.test         |  38 +
 clang/tools/CMakeLists.txt                    |   1 +
 clang/tools/ssaf-linker/CMakeLists.txt        |  14 +
 clang/tools/ssaf-linker/SSAFLinker.cpp        | 335 +++++++++
 .../Analysis/Scalable/EntityLinkerTest.cpp    |   3 +-
 .../SerializationFormatRegistryTest.cpp       |   2 +-
 .../JSONFormatTest/JSONFormatTest.cpp         |  61 +-
 .../JSONFormatTest/LUSummaryTest.cpp          |  20 +-
 .../JSONFormatTest/TUSummaryTest.cpp          |  20 +-
 38 files changed, 2649 insertions(+), 90 deletions(-)
 create mode 100644 clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
 create mode 100644 clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-badext.txt
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-empty.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-malformed.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-noext
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-empty.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/cli.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/help.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/io.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/linking.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/time.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/verbose.test
 create mode 100644 clang/tools/ssaf-linker/CMakeLists.txt
 create mode 100644 clang/tools/ssaf-linker/SSAFLinker.cpp

diff --git a/clang/include/clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h b/clang/include/clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h
index a38dd0c895452..0a18bd6d1d0f9 100644
--- a/clang/include/clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h
+++ b/clang/include/clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h
@@ -15,6 +15,7 @@
 #define LLVM_CLANG_ANALYSIS_SCALABLE_ENTITYLINKER_ENTITYSUMMARYENCODING_H
 
 #include "clang/Analysis/Scalable/Model/EntityId.h"
+#include "llvm/Support/Error.h"
 #include <map>
 
 namespace clang::ssaf {
@@ -32,7 +33,7 @@ class EntitySummaryEncoding {
   /// Updates EntityId references in the encoded data.
   ///
   /// \param EntityResolutionTable Mapping from old EntityIds to new EntityIds.
-  virtual void
+  virtual llvm::Error
   patch(const std::map<EntityId, EntityId> &EntityResolutionTable) = 0;
 };
 
diff --git a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
index d93fe41f67972..60e4eeb97edb6 100644
--- a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
+++ b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
@@ -28,27 +28,18 @@ class EntityIdTable;
 class EntitySummary;
 class SummaryName;
 
+/// Call this from main() to prevent the linker from dead-stripping the
+/// JSONFormat library and its static registration objects.
+void initializeJSONFormat();
+
 class JSONFormat final : public SerializationFormat {
   using Array = llvm::json::Array;
   using Object = llvm::json::Object;
+  using Value = llvm::json::Value;
 
-public:
-  // Helper class to provide limited access to EntityId conversion methods.
-  // Only exposes EntityId serialization/deserialization to format handlers.
-  class EntityIdConverter {
-  public:
-    EntityId fromJSON(uint64_t EntityIdIndex) const {
-      return Format.entityIdFromJSON(EntityIdIndex);
-    }
-
-    uint64_t toJSON(EntityId EI) const { return Format.entityIdToJSON(EI); }
-
-  private:
-    friend class JSONFormat;
-    EntityIdConverter(const JSONFormat &Format) : Format(Format) {}
-    const JSONFormat &Format;
-  };
+  friend class JSONEntitySummaryEncoding;
 
+public:
   llvm::Expected<TUSummary> readTUSummary(llvm::StringRef Path) override;
 
   llvm::Error writeTUSummary(const TUSummary &Summary,
@@ -71,11 +62,15 @@ class JSONFormat final : public SerializationFormat {
   llvm::Error writeLUSummaryEncoding(const LUSummaryEncoding &SummaryEncoding,
                                      llvm::StringRef Path) override;
 
-  using SerializerFn = llvm::function_ref<Object(const EntitySummary &,
-                                                 const EntityIdConverter &)>;
+  using EntityIdToJSONFn = llvm::function_ref<Object(EntityId)>;
+  using EntityIdFromJSONFn =
+      llvm::function_ref<llvm::Expected<EntityId>(const Object &)>;
+
+  using SerializerFn =
+      llvm::function_ref<Object(const EntitySummary &, EntityIdToJSONFn)>;
   using DeserializerFn =
       llvm::function_ref<llvm::Expected<std::unique_ptr<EntitySummary>>(
-          const Object &, EntityIdTable &, const EntityIdConverter &)>;
+          const Object &, EntityIdTable &, EntityIdFromJSONFn)>;
 
   using FormatInfo = FormatInfoEntry<SerializerFn, DeserializerFn>;
 
@@ -86,6 +81,11 @@ class JSONFormat final : public SerializationFormat {
   EntityId entityIdFromJSON(const uint64_t EntityIdIndex) const;
   uint64_t entityIdToJSON(EntityId EI) const;
 
+  llvm::Expected<EntityId>
+  entityIdFromJSONObject(const Object &EntityIdObject) const;
+  static Value *entityIdReferenceFromJSONObject(Object &EntityIdObject);
+  Object entityIdToJSONObject(EntityId EI) const;
+
   llvm::Expected<BuildNamespace>
   buildNamespaceFromJSON(const Object &BuildNamespaceObject) const;
   Object buildNamespaceToJSON(const BuildNamespace &BN) const;
diff --git a/clang/include/clang/Analysis/Scalable/Serialization/SerializationFormat.h b/clang/include/clang/Analysis/Scalable/Serialization/SerializationFormat.h
index c86ca7ef960e9..55e1ec7addc6c 100644
--- a/clang/include/clang/Analysis/Scalable/Serialization/SerializationFormat.h
+++ b/clang/include/clang/Analysis/Scalable/Serialization/SerializationFormat.h
@@ -58,7 +58,7 @@ class SerializationFormat {
   // Helpers providing access to implementation details of basic data structures
   // for efficient serialization/deserialization.
 
-  EntityId makeEntityId(const size_t Index) const { return EntityId(Index); }
+  static EntityId makeEntityId(const size_t Index) { return EntityId(Index); }
 
 #define FIELD(CLASS, FIELD_NAME)                                               \
   static const auto &get##FIELD_NAME(const CLASS &X) { return X.FIELD_NAME; }  \
diff --git a/clang/lib/Analysis/Scalable/CMakeLists.txt b/clang/lib/Analysis/Scalable/CMakeLists.txt
index 81550df4565cb..52f56fa6261ed 100644
--- a/clang/lib/Analysis/Scalable/CMakeLists.txt
+++ b/clang/lib/Analysis/Scalable/CMakeLists.txt
@@ -11,6 +11,7 @@ add_clang_library(clangAnalysisScalable
   Model/EntityLinkage.cpp
   Model/EntityName.cpp
   Model/SummaryName.cpp
+  Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
   Serialization/JSONFormat/JSONFormatImpl.cpp
   Serialization/JSONFormat/LUSummary.cpp
   Serialization/JSONFormat/LUSummaryEncoding.cpp
diff --git a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
index cd0c83a38a377..5449dada64c68 100644
--- a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
+++ b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
@@ -166,7 +166,13 @@ void EntityLinker::patch(
     const std::map<EntityId, EntityId> &EntityResolutionTable) {
   for (auto *PatchTarget : PatchTargets) {
     assert(PatchTarget && "EntityLinker::patch: Patch target cannot be null");
-    PatchTarget->patch(EntityResolutionTable);
+
+    if (auto Err = PatchTarget->patch(EntityResolutionTable)) {
+      std::string PatchingErrorMessage = llvm::toString(std::move(Err));
+      ErrorBuilder::fatal("{0} - {1}",
+                          ErrorMessages::EntityLinkerFatalErrorPrefix,
+                          PatchingErrorMessage);
+    }
   }
 }
 
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
new file mode 100644
index 0000000000000..13a20ed1800a4
--- /dev/null
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
@@ -0,0 +1,68 @@
+//===- JSONEntitySummaryEncoding.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 "JSONEntitySummaryEncoding.h"
+#include "JSONFormatImpl.h"
+
+namespace clang::ssaf {
+
+llvm::Error JSONEntitySummaryEncoding::patchObject(
+    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
+
+  if (auto AtVal = JSONFormat::entityIdReferenceFromJSONObject(Obj)) {
+    std::optional<uint64_t> OptEntityIdIndex = AtVal->getAsUINT64();
+    if (!OptEntityIdIndex) {
+      return ErrorBuilder::create(std::errc::invalid_argument,
+                                  ErrorMessages::FailedToReadEntityIdObject,
+                                  JSONEntityIdKey)
+          .build();
+    }
+
+    auto OldId = JSONFormat::makeEntityId(*OptEntityIdIndex);
+    auto It = Table.find(OldId);
+    if (It == Table.end()) {
+      return ErrorBuilder::create(
+                 std::errc::invalid_argument,
+                 ErrorMessages::FailedToPatchEntityIdNotInTable, OldId)
+          .build();
+    }
+
+    *AtVal = static_cast<uint64_t>(JSONFormat::getIndex(It->second));
+  } else {
+    for (auto &[Key, Val] : Obj) {
+      if (auto Err = patchValue(Val, Table)) {
+        return Err;
+      }
+    }
+  }
+
+  return llvm::Error::success();
+}
+
+llvm::Error JSONEntitySummaryEncoding::patchValue(
+    llvm::json::Value &V, const std::map<EntityId, EntityId> &Table) {
+  if (llvm::json::Object *Obj = V.getAsObject()) {
+    if (auto Err = patchObject(*Obj, Table)) {
+      return Err;
+    }
+  } else if (llvm::json::Array *Arr = V.getAsArray()) {
+    for (auto &Val : *Arr) {
+      if (auto Err = patchValue(Val, Table)) {
+        return Err;
+      }
+    }
+  }
+  return llvm::Error::success();
+}
+
+llvm::Error JSONEntitySummaryEncoding::patch(
+    const std::map<EntityId, EntityId> &EntityResolutionTable) {
+  return patchValue(Data, EntityResolutionTable);
+}
+
+} // namespace clang::ssaf
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
new file mode 100644
index 0000000000000..92fec92650c4c
--- /dev/null
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
@@ -0,0 +1,55 @@
+//===- JSONEntitySummaryEncoding.h ------------------------------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// Opaque JSON-based entity summary encoding used by JSONFormat. Stores raw
+// EntitySummary JSON blobs and patches embedded entity ID references without
+// requiring knowledge of the analysis schema.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef CLANG_LIB_ANALYSIS_SCALABLE_SERIALIZATION_JSONFORMAT_JSONENTITYSUMMARYENCODING_H
+#define CLANG_LIB_ANALYSIS_SCALABLE_SERIALIZATION_JSONFORMAT_JSONENTITYSUMMARYENCODING_H
+
+#include "clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h"
+#include "clang/Analysis/Scalable/Serialization/JSONFormat.h"
+#include "llvm/Support/JSON.h"
+
+#include <map>
+
+namespace clang::ssaf {
+
+//----------------------------------------------------------------------------
+// JSONEntitySummaryEncoding
+//
+// Concrete EntitySummaryEncoding used by JSONFormat for both TUSummaryEncoding
+// and LUSummaryEncoding. Stores the raw EntitySummary JSON value opaquely so
+// the linker can patch and emit it without knowing the analysis schema.
+//----------------------------------------------------------------------------
+
+class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
+  friend JSONFormat;
+
+public:
+  llvm::Error
+  patch(const std::map<EntityId, EntityId> &EntityResolutionTable) override;
+
+private:
+  explicit JSONEntitySummaryEncoding(llvm::json::Value Data)
+      : Data(std::move(Data)) {}
+
+  static llvm::Error patchObject(llvm::json::Object &Obj,
+                                 const std::map<EntityId, EntityId> &Table);
+  static llvm::Error patchValue(llvm::json::Value &V,
+                                const std::map<EntityId, EntityId> &Table);
+
+  llvm::json::Value Data;
+};
+
+} // namespace clang::ssaf
+
+#endif // CLANG_LIB_ANALYSIS_SCALABLE_SERIALIZATION_JSONFORMAT_JSONENTITYSUMMARYENCODING_H
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
index 0cf35fdae6927..5f49ee5c10898 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -8,13 +8,19 @@
 
 #include "JSONFormatImpl.h"
 
+#include "clang/Analysis/Scalable/Serialization/SerializationFormatRegistry.h"
 #include "clang/Analysis/Scalable/TUSummary/TUSummary.h"
 #include "llvm/Support/Registry.h"
 
 LLVM_INSTANTIATE_REGISTRY(llvm::Registry<clang::ssaf::JSONFormat::FormatInfo>)
 
+static clang::ssaf::SerializationFormatRegistry::Add<clang::ssaf::JSONFormat>
+    RegisterJSONFormat("JSON", "JSON serialization format");
+
 namespace clang::ssaf {
 
+void initializeJSONFormat() {}
+
 //----------------------------------------------------------------------------
 // JSON Reader and Writer
 //----------------------------------------------------------------------------
@@ -142,6 +148,53 @@ uint64_t JSONFormat::entityIdToJSON(EntityId EI) const {
   return static_cast<uint64_t>(getIndex(EI));
 }
 
+llvm::Expected<EntityId>
+JSONFormat::entityIdFromJSONObject(const Object &EntityIdObject) const {
+  if (EntityIdObject.size() != 1) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadEntityIdObject,
+                                JSONEntityIdKey)
+        .build();
+  }
+
+  const llvm::json::Value *AtVal = EntityIdObject.get(JSONEntityIdKey);
+  if (!AtVal) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadEntityIdObject,
+                                JSONEntityIdKey)
+        .build();
+  }
+
+  std::optional<uint64_t> OptEntityIdIndex = AtVal->getAsUINT64();
+  if (!OptEntityIdIndex) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadEntityIdObject,
+                                JSONEntityIdKey)
+        .build();
+  }
+
+  return makeEntityId(static_cast<size_t>(*OptEntityIdIndex));
+}
+
+Value *JSONFormat::entityIdReferenceFromJSONObject(Object &EntityIdObject) {
+  if (EntityIdObject.size() != 1) {
+    return nullptr;
+  }
+
+  llvm::json::Value *AtVal = EntityIdObject.get(JSONEntityIdKey);
+  if (!AtVal) {
+    return nullptr;
+  }
+
+  return AtVal;
+}
+
+Object JSONFormat::entityIdToJSONObject(EntityId EI) const {
+  Object Result;
+  Result["@"] = static_cast<uint64_t>(getIndex(EI));
+  return Result;
+}
+
 //----------------------------------------------------------------------------
 // BuildNamespaceKind
 //----------------------------------------------------------------------------
@@ -611,8 +664,9 @@ JSONFormat::entitySummaryFromJSON(const SummaryName &SN,
   const auto &InfoEntry = InfoIt->second;
   assert(InfoEntry.ForSummary == SN);
 
-  EntityIdConverter Converter(*this);
-  return InfoEntry.Deserialize(EntitySummaryObject, IdTable, Converter);
+  return InfoEntry.Deserialize(
+      EntitySummaryObject, IdTable,
+      [this](const Object &Obj) { return entityIdFromJSONObject(Obj); });
 }
 
 llvm::Expected<Object>
@@ -629,8 +683,8 @@ JSONFormat::entitySummaryToJSON(const SummaryName &SN,
   const auto &InfoEntry = InfoIt->second;
   assert(InfoEntry.ForSummary == SN);
 
-  EntityIdConverter Converter(*this);
-  return InfoEntry.Serialize(ES, Converter);
+  return InfoEntry.Serialize(
+      ES, [this](EntityId EI) { return entityIdToJSONObject(EI); });
 }
 
 //----------------------------------------------------------------------------
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
index b51313c5e6877..74837d845b8f1 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
@@ -15,6 +15,7 @@
 #define CLANG_LIB_ANALYSIS_SCALABLE_SERIALIZATION_JSONFORMAT_JSONFORMATIMPL_H
 
 #include "../../ModelStringConversions.h"
+#include "JSONEntitySummaryEncoding.h"
 #include "clang/Analysis/Scalable/EntityLinker/EntitySummaryEncoding.h"
 #include "clang/Analysis/Scalable/Model/EntityLinkage.h"
 #include "clang/Analysis/Scalable/Serialization/JSONFormat.h"
@@ -108,31 +109,25 @@ inline constexpr const char *FailedToDeserializeLinkageTableExtraId =
 inline constexpr const char *FailedToDeserializeLinkageTableMissingId =
     "failed to deserialize LinkageTable: missing '{0}' present in IdTable";
 
+inline constexpr const char *FailedToReadEntityIdObject =
+    "failed to read EntityId: expected JSON object with a single '{0}' key "
+    "mapped to a number (unsigned 64-bit integer)";
+
+inline constexpr const char *FailedToPatchEntityIdNotInTable =
+    "failed to patch EntityId: '{0}' not found in entity resolution table";
+
 } // namespace ErrorMessages
 
 //----------------------------------------------------------------------------
-// JSONEntitySummaryEncoding
-//
-// Concrete EntitySummaryEncoding used by JSONFormat for both TUSummaryEncoding
-// and LUSummaryEncoding. Stores the raw EntitySummary JSON value opaquely so
-// the linker can patch and emit it without knowing the analysis schema.
+// Entity Id JSON Representation
 //----------------------------------------------------------------------------
 
-class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
-  friend JSONFormat;
-
-public:
-  void
-  patch(const std::map<EntityId, EntityId> &EntityResolutionTable) override {
-    ErrorBuilder::fatal("will be implemented in the future");
-  }
+namespace {
 
-private:
-  explicit JSONEntitySummaryEncoding(llvm::json::Value Data)
-      : Data(std::move(Data)) {}
+/// An entity ID is encoded as the single-key object {"@": <index>}.
+inline constexpr const char *JSONEntityIdKey = "@";
 
-  llvm::json::Value Data;
-};
+} // namespace
 
 //----------------------------------------------------------------------------
 // JSON Reader and Writer
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
new file mode 100644
index 0000000000000..b67f9aad690c3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
@@ -0,0 +1,159 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu1.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu1#"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu1#"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu1#"
+      }
+    }
+  ],
+  "linkage_table": [
+    { "id": 0, "linkage": { "type": "External" } },
+    { "id": 1, "linkage": { "type": "Internal" } },
+    { "id": 2, "linkage": { "type": "None" } },
+    { "id": 3, "linkage": { "type": "External" } },
+    { "id": 4, "linkage": { "type": "Internal" } },
+    { "id": 5, "linkage": { "type": "None" } }
+  ],
+  "data": [
+    {
+      "summary_name": "CallGraph",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 3 }] }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 4 }] }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": { "call_count": 1, "callees": [{ "@": 5 }] }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 1 }] }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": { "call_count": 1, "callees": [{ "@": 3 }] }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": { "call_count": 3, "callees": [{ "@": 0 }, { "@": 3 }, { "@": 4 }] }
+        }
+      ]
+    },
+    {
+      "summary_name": "TypeInfo",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "direct": { "@": 3 },
+            "indirect": [
+              { "entity": { "@": 1 }, "level": 1 },
+              { "entity": { "@": 4 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "direct": { "@": 0 },
+            "indirect": [
+              { "entity": { "@": 2 }, "level": 1 },
+              { "entity": { "@": 5 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "direct": { "@": 1 },
+            "indirect": [
+              { "entity": { "@": 3 }, "level": 1 }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "direct": { "@": 4 },
+            "indirect": [
+              { "entity": { "@": 0 }, "level": 1 },
+              { "entity": { "@": 2 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "direct": { "@": 5 },
+            "indirect": [
+              { "entity": { "@": 1 }, "level": 1 }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "direct": { "@": 2 },
+            "indirect": [
+              { "entity": { "@": 0 }, "level": 1 },
+              { "entity": { "@": 3 }, "level": 2 },
+              { "entity": { "@": 4 }, "level": 3 }
+            ]
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
new file mode 100644
index 0000000000000..ac6e385e6b49d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
@@ -0,0 +1,160 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu2.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu2#"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu2#"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu2#"
+      }
+    }
+  ],
+  "linkage_table": [
+    { "id": 0, "linkage": { "type": "External" } },
+    { "id": 1, "linkage": { "type": "Internal" } },
+    { "id": 2, "linkage": { "type": "None" } },
+    { "id": 3, "linkage": { "type": "External" } },
+    { "id": 4, "linkage": { "type": "Internal" } },
+    { "id": 5, "linkage": { "type": "None" } }
+  ],
+  "data": [
+    {
+      "summary_name": "CallGraph",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 5 }] }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 3 }] }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": { "call_count": 1, "callees": [{ "@": 4 }] }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 2 }] }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": { "call_count": 1, "callees": [{ "@": 3 }] }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 3 }] }
+        }
+      ]
+    },
+    {
+      "summary_name": "TypeInfo",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "direct": { "@": 3 },
+            "indirect": [
+              { "entity": { "@": 2 }, "level": 1 },
+              { "entity": { "@": 5 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "direct": { "@": 0 },
+            "indirect": [
+              { "entity": { "@": 4 }, "level": 1 }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "direct": { "@": 5 },
+            "indirect": [
+              { "entity": { "@": 0 }, "level": 1 },
+              { "entity": { "@": 3 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "direct": { "@": 1 },
+            "indirect": [
+              { "entity": { "@": 2 }, "level": 1 },
+              { "entity": { "@": 0 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "direct": { "@": 2 },
+            "indirect": [
+              { "entity": { "@": 5 }, "level": 1 },
+              { "entity": { "@": 3 }, "level": 2 }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "direct": { "@": 4 },
+            "indirect": [
+              { "entity": { "@": 1 }, "level": 1 },
+              { "entity": { "@": 2 }, "level": 2 },
+              { "entity": { "@": 0 }, "level": 3 }
+            ]
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-badext.txt b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-badext.txt
new file mode 100644
index 0000000000000..a2466bf0a9eb9
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-badext.txt
@@ -0,0 +1 @@
+This file exists to exercise the unsupported-extension error path in cli.test.
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
new file mode 100644
index 0000000000000..85e884b96590b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
@@ -0,0 +1,28 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu-dup-id.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu-dup-id.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at foo#"
+      }
+    },
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu-dup-id.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at bar#"
+      }
+    }
+  ],
+  "linkage_table": [
+    { "id": 0, "linkage": { "type": "External" } }
+  ],
+  "data": []
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
new file mode 100644
index 0000000000000..4550e814bceab
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
@@ -0,0 +1,9 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu1.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [],
+  "data": []
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-empty.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-empty.json
new file mode 100644
index 0000000000000..ec1d262f42653
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-empty.json
@@ -0,0 +1,9 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "empty.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [],
+  "data": []
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-malformed.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-malformed.json
new file mode 100644
index 0000000000000..88b85f4dd1a1b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-malformed.json
@@ -0,0 +1 @@
+this is not valid json {{{
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-noext b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-noext
new file mode 100644
index 0000000000000..6b1b7f06742ea
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-noext
@@ -0,0 +1 @@
+This file exists to exercise the no-extension error path in cli.test.
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json
new file mode 100644
index 0000000000000..f0aa5507ec19a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json
@@ -0,0 +1,30 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "tu-orphan-data.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{ "kind": "CompilationUnit", "name": "tu-orphan-data.cpp" }],
+        "suffix": "",
+        "usr": "c:@F at foo#"
+      }
+    }
+  ],
+  "linkage_table": [
+    { "id": 0, "linkage": { "type": "External" } }
+  ],
+  "data": [
+    {
+      "summary_name": "TestAnalysis",
+      "summary_data": [
+        {
+          "entity_id": 99,
+          "entity_summary": {}
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
new file mode 100644
index 0000000000000..6a74d1e341c2c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
@@ -0,0 +1,652 @@
+{
+  "data": [
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 5
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 3
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 6,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 8
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 7,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 9
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 8,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 7
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 9,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 8
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 10,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 6
+              },
+              {
+                "@": 7
+              },
+              {
+                "@": 8
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "CallGraph"
+    },
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "direct": {
+              "@": 3
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "direct": {
+              "@": 0
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "direct": {
+              "@": 1
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "direct": {
+              "@": 4
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "direct": {
+              "@": 5
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "direct": {
+              "@": 2
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 6,
+          "entity_summary": {
+            "direct": {
+              "@": 0
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 9
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 7,
+          "entity_summary": {
+            "direct": {
+              "@": 10
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 8
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 8,
+          "entity_summary": {
+            "direct": {
+              "@": 6
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 7
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 9,
+          "entity_summary": {
+            "direct": {
+              "@": 7
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 10
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 8
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 10,
+          "entity_summary": {
+            "direct": {
+              "@": 9
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 6
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 7
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 3
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "TypeInfo"
+    }
+  ],
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 6,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 7,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu1#"
+      }
+    },
+    {
+      "id": 8,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu2#"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu1#"
+      }
+    },
+    {
+      "id": 9,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu2#"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu1#"
+      }
+    },
+    {
+      "id": 10,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1+2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu2#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 6,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 7,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 8,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 9,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 10,
+      "linkage": {
+        "type": "None"
+      }
+    }
+  ],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "lu-1+2"
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
new file mode 100644
index 0000000000000..3e5e17ce3f0c0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
@@ -0,0 +1,364 @@
+{
+  "data": [
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 5
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 3
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "CallGraph"
+    },
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "direct": {
+              "@": 3
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "direct": {
+              "@": 0
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "direct": {
+              "@": 1
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "direct": {
+              "@": 4
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "direct": {
+              "@": 5
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "direct": {
+              "@": 2
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 3
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "TypeInfo"
+    }
+  ],
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu1#"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu1#"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-1"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu1#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "type": "None"
+      }
+    }
+  ],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "lu-1"
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
new file mode 100644
index 0000000000000..a60887a53cb83
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
@@ -0,0 +1,370 @@
+{
+  "data": [
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 5
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 4
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 3
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "CallGraph"
+    },
+    {
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "direct": {
+              "@": 3
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 1,
+          "entity_summary": {
+            "direct": {
+              "@": 0
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 1
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 2,
+          "entity_summary": {
+            "direct": {
+              "@": 5
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 3,
+          "entity_summary": {
+            "direct": {
+              "@": 1
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 4,
+          "entity_summary": {
+            "direct": {
+              "@": 2
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              }
+            ]
+          }
+        },
+        {
+          "entity_id": 5,
+          "entity_summary": {
+            "direct": {
+              "@": 4
+            },
+            "indirect": [
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 3
+              }
+            ]
+          }
+        }
+      ],
+      "summary_name": "TypeInfo"
+    }
+  ],
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_ext#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_int#"
+      }
+    },
+    {
+      "id": 2,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at shared_none#"
+      }
+    },
+    {
+      "id": 3,
+      "name": {
+        "namespace": [
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_ext_tu2#"
+      }
+    },
+    {
+      "id": 4,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_int_tu2#"
+      }
+    },
+    {
+      "id": 5,
+      "name": {
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          },
+          {
+            "kind": "LinkUnit",
+            "name": "lu-2"
+          }
+        ],
+        "suffix": "",
+        "usr": "c:@F at unique_none_tu2#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "type": "None"
+      }
+    }
+  ],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "lu-2"
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-empty.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-empty.json
new file mode 100644
index 0000000000000..e63b1f0a1337d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-empty.json
@@ -0,0 +1,11 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "lu-empty"
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/cli.test b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
new file mode 100644
index 0000000000000..87302e78187b8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
@@ -0,0 +1,13 @@
+// Tests for ssaf-linker command-line option validation.
+
+// RUN: not ssaf-linker 2>&1 | FileCheck %s --check-prefix=NO-ARGS
+// NO-ARGS: ssaf-linker: Not enough positional command line arguments specified!
+// NO-ARGS: Must specify at least 1 positional argument: See: ssaf-linker --help
+
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 | FileCheck %s --check-prefix=NO-OUTPUT
+// NO-OUTPUT: ssaf-linker: for the --output option: must be specified at least once!
+
+// RUN: not ssaf-linker -o %t/output.json 2>&1 | FileCheck %s --check-prefix=NO-INPUT
+// NO-INPUT: ssaf-linker: Not enough positional command line arguments specified!
+// NO-INPUT: Must specify at least 1 positional argument: See: ssaf-linker --help
+
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
new file mode 100644
index 0000000000000..bc90d79435bc7
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -0,0 +1,23 @@
+// Test ssaf-linker help option
+
+// RUN: ssaf-linker --help-list-hidden | FileCheck %s
+
+// CHECK: OVERVIEW: SSAF Linker
+// CHECK-EMPTY:
+// CHECK: USAGE: ssaf-linker [options] input...
+// CHECK-EMPTY:
+// CHECK: OPTIONS:
+// CHECK:   -h                  - Alias for --help
+// CHECK:   --help              - Display available options (--help-hidden for more)
+// CHECK:   --help-hidden       - Display all available options
+// CHECK:   --help-list         - Display list of available options (--help-list-hidden for more)
+// CHECK:   --help-list-hidden  - Display list of all available options
+// CHECK:   -o                  -
+// CHECK:   --output=<path>     - Output summary path
+// CHECK:   --print-all-options - Print all option values after command line parsing
+// CHECK:   --print-options     - Print non-default options after command line parsing
+// CHECK:   -t                  -
+// CHECK:   --time              - Enable timing
+// CHECK:   -v                  -
+// CHECK:   --verbose           - Enable verbose output
+// CHECK:   --version           - Display the version of this program
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/io.test b/clang/test/Analysis/Scalable/ssaf-linker/io.test
new file mode 100644
index 0000000000000..386e64602c251
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/io.test
@@ -0,0 +1,26 @@
+// Tests for ssaf-linker file I/O error handling.
+
+// Input file is a directory.
+// RUN: not ssaf-linker %S/Inputs -o %t.json 2>&1 | FileCheck %s --check-prefix=INPUT-IS-DIR
+// INPUT-IS-DIR: error: Failed to load input summary
+// INPUT-IS-DIR: path is a directory, not a file
+
+// Input file has wrong extension — caught at validation before read.
+// (Extension check happens in SummaryFile::FromPath before real_path.)
+// RUN: not ssaf-linker %S/Inputs/empty.json %S/Inputs/malformed.json -o %t.json 2>&1
+
+// Malformed JSON input.
+// RUN: not ssaf-linker %S/Inputs/malformed.json -o %t.json 2>&1 | FileCheck %s --check-prefix=BAD-JSON
+// BAD-JSON: error: Failed to load input summary
+// BAD-JSON: failed to read file
+
+// Missing required fields in otherwise valid JSON.
+// RUN: not ssaf-linker %S/Inputs/missing-fields.json -o %t.json 2>&1 | FileCheck %s --check-prefix=MISSING-FIELDS
+// MISSING-FIELDS: error: Failed to load input summary
+// MISSING-FIELDS: failed to read
+
+// Output file already exists.
+// RUN: ssaf-linker %S/Inputs/empty.json -o %t-exists.json
+// RUN: not ssaf-linker %S/Inputs/empty.json -o %t-exists.json 2>&1 | FileCheck %s --check-prefix=OUTPUT-EXISTS
+// OUTPUT-EXISTS: error: Failed to write output summary
+// OUTPUT-EXISTS: file already exists
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
new file mode 100644
index 0000000000000..19b08936cef3d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
@@ -0,0 +1,9 @@
+// Tests for EntityLinker error conditions.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// Linking the same TU namespace twice produces an error.
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json %S/Inputs/tu-empty.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-NS -DPATH=%S/Inputs/tu-empty.json
+// DUP-NS: ssaf-linker: error: Failed to link input summary '[[PATH]]': failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp).
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking.test b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
new file mode 100644
index 0000000000000..63e501811334d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
@@ -0,0 +1,30 @@
+// Tests for successful ssaf-linker linking behaviour.
+// See linking-errors.test for error cases.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// Single empty TU produces valid output.
+// RUN: ssaf-linker %S/Inputs/tu-empty.json -o %t/lu-empty.json
+// RUN: diff %S/Outputs/lu-empty.json %t/lu-empty.json
+// RUN: rm %t/lu-empty.json
+
+// Single non-empty TU produces valid output.
+// RUN: ssaf-linker %S/Inputs/tu-1.json -o %t/lu-1.json
+// RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
+// RUN: rm %t/lu-1.json
+
+// Linking empty and non-empty TU is equivalent to just linking non-empty TU
+// RUN: ssaf-linker %S/Inputs/tu-empty.json %S/Inputs/tu-1.json -o %t/lu-1.json
+// RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
+// RUN: rm %t/lu-1.json
+
+// Linking non-empty and empty TU is equivalent to just linking non-empty TU
+// RUN: ssaf-linker %S/Inputs/tu-2.json %S/Inputs/tu-empty.json -o %t/lu-2.json
+// RUN: diff %S/Outputs/lu-2.json %t/lu-2.json
+// RUN: rm %t/lu-2.json
+
+// Linking two TUs correctly resolves and patches data.
+// RUN: ssaf-linker %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/time.test b/clang/test/Analysis/Scalable/ssaf-linker/time.test
new file mode 100644
index 0000000000000..0f7033876313f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/time.test
@@ -0,0 +1,32 @@
+// Test the --time/-t flag of ssaf-linker.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// RUN: ssaf-linker --time %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
+// RUN:           | FileCheck %s --check-prefix=CHECK-LONG
+// CHECK-LONG: ===-------------------------------------------------------------------------===
+// CHECK-LONG:                                   SSAF Linker
+// CHECK-LONG: ===-------------------------------------------------------------------------===
+// CHECK-LONG: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
+// CHECK-LONG: ---User Time---   --System Time--   --User+System--   ---Wall Time---
+// CHECK-LONG-DAG: {{.*}}Write Summary
+// CHECK-LONG-DAG: {{.*}}Read Summaries
+// CHECK-LONG-DAG: {{.*}}Link Summaries
+// CHECK-LONG-DAG: {{.*}}Validate Input
+// CHECK-LONG: {{.*}}Total
+
+// RUN: rm %t/lu-1+2.json
+
+// RUN: ssaf-linker -t %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
+// RUN:           | FileCheck %s --check-prefix=CHECK-SHORT
+// CHECK-SHORT: ===-------------------------------------------------------------------------===
+// CHECK-SHORT:                                   SSAF Linker
+// CHECK-SHORT: ===-------------------------------------------------------------------------===
+// CHECK-SHORT: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
+// CHECK-SHORT: ---User Time---   --System Time--   --User+System--   ---Wall Time---
+// CHECK-SHORT-DAG: {{.*}}Write Summary
+// CHECK-SHORT-DAG: {{.*}}Read Summaries
+// CHECK-SHORT-DAG: {{.*}}Link Summaries
+// CHECK-SHORT-DAG: {{.*}}Validate Input
+// CHECK-SHORT: {{.*}}Total
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
new file mode 100644
index 0000000000000..1fc99a18e8041
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -0,0 +1,49 @@
+// Tests for EntityLinker error cases.
+
+// RUN: mkdir -p %t
+
+// No extension on output.
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-EXT-OUTPUT
+// NO-EXT-OUTPUT: ssaf-linker: error: Failed to validate summary 'lu': Extension not supplied.
+
+// No extension on input.
+// RUN: not ssaf-linker %S/Inputs/tu-noext -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-EXT-INPUT -DPATH=%S/Inputs/tu-noext
+// NO-EXT-INPUT: ssaf-linker: error: Failed to validate summary '[[PATH]]': Extension not supplied.
+
+// Invalid extension on output.
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu.txt 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-EXT-OUTPUT
+// BAD-EXT-OUTPUT: ssaf-linker: error: Failed to validate summary 'lu.txt': Format not registered for extension 'txt'.
+
+// Invalid extension on input.
+// RUN: not ssaf-linker %S/Inputs/tu-badext.txt -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-EXT-INPUT -DPATH=%S/Inputs/tu-badext.txt
+// BAD-EXT-INPUT: ssaf-linker: error: Failed to validate summary '[[PATH]]': Format not registered for extension 'txt'.
+
+// Output directory does not exist.
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %S/Outputs/NonExistentDirectory/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUTPUT-PARENT-DIR-MISSING -DPATH=%S/Outputs/NonExistentDirectory/lu.json
+// OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: Failed to validate summary '[[PATH]]': Parent directory does not exist.
+
+// Output parent directory exists but is not writable.
+// UNSUPPORTED: system-windows
+// RUN: mkdir -p %t/output-dir
+// RUN: chmod -w %t/output-dir
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/output-dir/output.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-WRITE-PERM -DPATH=%t/output-dir/output.json
+// RUN: chmod +w %t/output-dir
+// NO-WRITE-PERM: ssaf-linker: error: Failed to validate summary '[[PATH]]': Parent directory is not writable.
+
+// Input summary does not exist.
+// RUN: not ssaf-linker %S/Inputs/tu-nonexistent.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-INPUT-FILE -DPATH=%S/Inputs/tu-nonexistent.json
+// NO-INPUT-FILE: ssaf-linker: error: Failed to validate summary '[[PATH]]': No such file or directory.
+
+// Input summary is a broken symlink.
+// RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
+// RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BROKEN-SYMLINK -DPATH=%t/tu-dangling.json
+// BROKEN-SYMLINK: ssaf-linker: error: Failed to validate summary '[[PATH]]': No such file or directory.
+
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
new file mode 100644
index 0000000000000..81c8c922e5324
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
@@ -0,0 +1,38 @@
+// Test the --verbose/-v flag of ssaf-linker.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// RUN: ssaf-linker --verbose %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
+// RUN:           | FileCheck %s --check-prefix=CHECK-LONG -DINPUT=%S/Inputs -DOUTPUT=%t
+// CHECK-LONG: note: - Linking started.
+// CHECK-LONG: note:   - Validating input.
+// CHECK-LONG: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
+// CHECK-LONG: note:     - Validated 2 input summary paths.
+// CHECK-LONG: note:   - Linking input.
+// CHECK-LONG: note:     - Constructing linker.
+// CHECK-LONG: note:     - Linking summaries.
+// CHECK-LONG: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
+// CHECK-LONG: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
+// CHECK-LONG: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
+// CHECK-LONG: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
+// CHECK-LONG: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
+// CHECK-LONG: note: - Linking finished.
+
+// RUN: rm %t/lu-1+2.json
+
+// RUN: ssaf-linker -v %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
+// RUN:           | FileCheck %s --check-prefix=CHECK-SHORT -DINPUT=%S/Inputs -DOUTPUT=%t
+// CHECK-SHORT: note: - Linking started.
+// CHECK-SHORT: note:   - Validating input.
+// CHECK-SHORT: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
+// CHECK-SHORT: note:     - Validated 2 input summary paths.
+// CHECK-SHORT: note:   - Linking input.
+// CHECK-SHORT: note:     - Constructing linker.
+// CHECK-SHORT: note:     - Linking summaries.
+// CHECK-SHORT: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
+// CHECK-SHORT: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
+// CHECK-SHORT: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
+// CHECK-SHORT: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
+// CHECK-SHORT: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
+// CHECK-SHORT: note: - Linking finished.
\ No newline at end of file
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index afdd613b4ee99..aa4c4c226db57 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -53,3 +53,4 @@ add_llvm_external_project(clang-tools-extra extra)
 add_clang_subdirectory(libclang)
 
 add_clang_subdirectory(offload-arch)
+add_clang_subdirectory(ssaf-linker)
diff --git a/clang/tools/ssaf-linker/CMakeLists.txt b/clang/tools/ssaf-linker/CMakeLists.txt
new file mode 100644
index 0000000000000..db5750fb1f758
--- /dev/null
+++ b/clang/tools/ssaf-linker/CMakeLists.txt
@@ -0,0 +1,14 @@
+set(LLVM_LINK_COMPONENTS
+  Option
+  Support
+  )
+
+add_clang_tool(ssaf-linker
+  SSAFLinker.cpp
+  )
+
+clang_target_link_libraries(ssaf-linker
+  PRIVATE
+  clangAnalysisScalable
+  clangBasic
+  )
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
new file mode 100644
index 0000000000000..ad9ae56646260
--- /dev/null
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -0,0 +1,335 @@
+//===--- tools/ssaf-linker/SSAFLinker.cpp - SSAF Linker -------------------===//
+//
+// 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 entity linker tool that performs entity
+//  linking across multiple TU summaries using the EntityLinker framework.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Analysis/Scalable/EntityLinker/EntityLinker.h"
+#include "clang/Analysis/Scalable/EntityLinker/TUSummaryEncoding.h"
+#include "clang/Analysis/Scalable/Model/BuildNamespace.h"
+#include "clang/Analysis/Scalable/Serialization/JSONFormat.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/Signals.h"
+#include "llvm/Support/Timer.h"
+#include "llvm/Support/WithColor.h"
+#include "llvm/Support/raw_ostream.h"
+#include <map>
+#include <memory>
+#include <string>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace fs = llvm::sys::fs;
+namespace path = llvm::sys::path;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Command-Line Options
+//===----------------------------------------------------------------------===//
+
+cl::OptionCategory SsafLinkerCategory("ssaf-linker options");
+
+cl::list<std::string> InputPaths(cl::Positional, cl::desc("input..."),
+                                 cl::value_desc("path"), cl::OneOrMore,
+                                 cl::cat(SsafLinkerCategory));
+
+cl::opt<std::string> OutputPath("output", cl::desc("Output summary path"),
+                                cl::value_desc("path"), cl::Required,
+                                cl::cat(SsafLinkerCategory));
+
+cl::alias OutputFileShort("o", cl::aliasopt(OutputPath));
+
+cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
+                      cl::init(false), cl::cat(SsafLinkerCategory));
+
+cl::alias VerboseShort("v", cl::aliasopt(Verbose));
+
+cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
+                   cl::cat(SsafLinkerCategory));
+
+cl::alias TimeShort("t", cl::aliasopt(Time));
+
+//===----------------------------------------------------------------------===//
+// Error Messages
+//===----------------------------------------------------------------------===//
+
+constexpr const char *ErrorCannotValidateSummary =
+    "Failed to validate summary '{0}': {1}";
+
+constexpr const char *ErrorOutputDirectoryMissing =
+    "Parent directory does not exist.";
+
+constexpr const char *ErrorOutputDirectoryNotWritable =
+    "Parent directory is not writable.";
+
+constexpr const char *ErrorExtensionNotSupplied = "Extension not supplied.";
+
+constexpr const char *ErrorNoFormatForExtension =
+    "Format not registered for extension '{0}'.";
+
+constexpr const char *ErrorCannotResolvePath =
+    "Failed to validate summary '{0}': {1}.";
+
+constexpr const char *ErrorLoadFailed =
+    "Failed to load input summary '{0}': {1}.";
+
+constexpr const char *ErrorLinkFailed =
+    "Failed to link input summary '{0}': {1}.";
+
+constexpr const char *ErrorWriteFailed =
+    "Failed to write output summary '{0}': {1}.";
+
+//===----------------------------------------------------------------------===//
+// Diagnostic Utilities
+//===----------------------------------------------------------------------===//
+
+constexpr unsigned IndentationWidth = 2;
+
+static llvm::StringRef ToolName;
+
+template <typename... Ts>
+[[noreturn]] void Fail(const char *Fmt, Ts &&...Args) {
+  llvm::WithColor::error(llvm::errs(), ToolName)
+      << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
+  std::exit(1);
+}
+
+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";
+  }
+}
+
+struct ScopedTimer {
+  explicit ScopedTimer(llvm::Timer &T) : T(T) {
+    if (Time) {
+      T.startTimer();
+    }
+  }
+
+  ~ScopedTimer() {
+    if (Time) {
+      T.stopTimer();
+    }
+  }
+  llvm::Timer &T;
+};
+
+//===----------------------------------------------------------------------===//
+// Format Registry
+//===----------------------------------------------------------------------===//
+
+// TODO - will be replaced by an equivalent method from the framework
+std::unique_ptr<SerializationFormat>
+MakeFormatForExtension(llvm::StringRef Extension) {
+  if (Extension == "json") {
+    return std::make_unique<JSONFormat>();
+  }
+  return nullptr;
+}
+
+SerializationFormat *GetFormatForExtension(llvm::StringRef Extension) {
+  static std::map<std::string, std::unique_ptr<SerializationFormat>>
+      ExtensionFormatMap;
+
+  auto It = ExtensionFormatMap.find(Extension.str());
+  if (It != ExtensionFormatMap.end()) {
+    return It->second.get();
+  }
+
+  auto Format = MakeFormatForExtension(Extension);
+  SerializationFormat *Result = Format.get();
+
+  if (Result) {
+    ExtensionFormatMap.emplace(Extension, std::move(Format));
+  }
+
+  return Result;
+}
+
+//===----------------------------------------------------------------------===//
+// Data Structures
+//===----------------------------------------------------------------------===//
+
+struct SummaryFile {
+  std::string Path;
+  SerializationFormat *Format = nullptr;
+
+  static SummaryFile FromPath(llvm::StringRef Path) {
+    llvm::StringRef Extension = path::extension(Path);
+    if (Extension.empty()) {
+      Fail(ErrorCannotValidateSummary, Path, ErrorExtensionNotSupplied);
+    }
+    Extension = Extension.drop_front();
+    SerializationFormat *Format = GetFormatForExtension(Extension);
+    if (!Format) {
+      std::string Suffix = llvm::formatv(ErrorNoFormatForExtension, Extension);
+      Fail(ErrorCannotValidateSummary, Path, Suffix);
+    }
+    return {Path.str(), Format};
+  }
+};
+
+struct LinkerInput {
+  std::vector<SummaryFile> InputFiles;
+  SummaryFile OutputFile;
+  std::string LinkUnitName;
+};
+
+//===----------------------------------------------------------------------===//
+// Pipeline
+//===----------------------------------------------------------------------===//
+
+LinkerInput Validate(llvm::TimerGroup &TG) {
+  llvm::Timer TValidate("validate", "Validate Input", TG);
+  LinkerInput LI;
+
+  {
+    ScopedTimer _(TValidate);
+    llvm::StringRef ParentDir = path::parent_path(OutputPath);
+    llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
+
+    if (!fs::exists(DirToCheck)) {
+      Fail(ErrorCannotValidateSummary, OutputPath, ErrorOutputDirectoryMissing);
+    }
+
+    if (fs::access(DirToCheck, fs::AccessMode::Write)) {
+      Fail(ErrorCannotValidateSummary, OutputPath,
+           ErrorOutputDirectoryNotWritable);
+    }
+
+    LI.OutputFile = SummaryFile::FromPath(OutputPath);
+    LI.LinkUnitName = path::stem(LI.OutputFile.Path).str();
+  }
+
+  Info(2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
+
+  {
+    ScopedTimer _(TValidate);
+    for (const auto &InputPath : InputPaths) {
+      llvm::SmallString<256> RealPath;
+      std::error_code EC = fs::real_path(InputPath, RealPath, true);
+      if (EC) {
+        Fail(ErrorCannotResolvePath, InputPath, EC.message());
+      }
+      LI.InputFiles.push_back(SummaryFile::FromPath(RealPath));
+    }
+  }
+
+  Info(2, "Validated {0} input summary paths.", LI.InputFiles.size());
+
+  return LI;
+}
+
+void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
+  Info(2, "Constructing linker.");
+
+  EntityLinker EL(NestedBuildNamespace(
+      BuildNamespace(BuildNamespaceKind::LinkUnit, LI.LinkUnitName)));
+
+  llvm::Timer TRead("read", "Read Summaries", TG);
+  llvm::Timer TLink("link", "Link Summaries", TG);
+  llvm::Timer TWrite("write", "Write Summary", TG);
+
+  Info(2, "Linking summaries.");
+
+  for (auto [Index, InputFile] : llvm::enumerate(LI.InputFiles)) {
+    std::unique_ptr<TUSummaryEncoding> Summary;
+
+    {
+      Info(3, "[{0}/{1}] Reading '{2}'.", (Index + 1), LI.InputFiles.size(),
+           InputFile.Path);
+
+      ScopedTimer _(TRead);
+
+      auto ExpectedSummaryEncoding =
+          InputFile.Format->readTUSummaryEncoding(InputFile.Path);
+      if (!ExpectedSummaryEncoding) {
+        Fail(ErrorLoadFailed, InputFile.Path,
+             toString(ExpectedSummaryEncoding.takeError()));
+      }
+
+      Summary = std::make_unique<TUSummaryEncoding>(
+          std::move(*ExpectedSummaryEncoding));
+    }
+
+    {
+      Info(3, "[{0}/{1}] Linking '{2}'.", (Index + 1), LI.InputFiles.size(),
+           InputFile.Path);
+
+      ScopedTimer _(TLink);
+
+      if (auto Err = EL.link(std::move(Summary))) {
+        Fail(ErrorLinkFailed, InputFile.Path, toString(std::move(Err)));
+      }
+    }
+  }
+
+  {
+    Info(2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
+
+    ScopedTimer _(TWrite);
+
+    auto Output = std::move(EL).getOutput();
+    if (auto Err = LI.OutputFile.Format->writeLUSummaryEncoding(
+            Output, LI.OutputFile.Path)) {
+      Fail(ErrorWriteFailed, LI.OutputFile.Path, toString(std::move(Err)));
+    }
+  }
+}
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Driver
+//===----------------------------------------------------------------------===//
+
+int main(int argc, const char **argv) {
+  ToolName = argv[0];
+  initializeJSONFormat();
+  sys::PrintStackTraceOnErrorSignal(argv[0]);
+
+  cl::HideUnrelatedOptions(SsafLinkerCategory);
+  cl::ParseCommandLineOptions(argc, argv, "SSAF Linker\n");
+
+  llvm::TimerGroup LinkerTimers("ssaf-linker", "SSAF Linker");
+  LinkerInput LI;
+
+  {
+    Info(0, "Linking started.");
+
+    {
+      Info(1, "Validating input.");
+      LI = Validate(LinkerTimers);
+    }
+
+    {
+      Info(1, "Linking input.");
+      Link(LI, LinkerTimers);
+    }
+
+    Info(0, "Linking finished.");
+  }
+
+  if (Time) {
+    LinkerTimers.print(llvm::errs());
+  }
+
+  return 0;
+}
diff --git a/clang/unittests/Analysis/Scalable/EntityLinkerTest.cpp b/clang/unittests/Analysis/Scalable/EntityLinkerTest.cpp
index b6d28f701fb6f..93e276f0c0237 100644
--- a/clang/unittests/Analysis/Scalable/EntityLinkerTest.cpp
+++ b/clang/unittests/Analysis/Scalable/EntityLinkerTest.cpp
@@ -36,9 +36,10 @@ class MockEntitySummaryEncoding : public EntitySummaryEncoding {
 
   size_t getId() const { return Id; }
 
-  void
+  llvm::Error
   patch(const std::map<EntityId, EntityId> &EntityResolutionTable) override {
     PatchedIds = EntityResolutionTable;
+    return llvm::Error::success();
   }
 
   const std::map<EntityId, EntityId> &getPatchedIds() const {
diff --git a/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp b/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
index 8b6d1a5ae15cf..b8f1ea0a866e4 100644
--- a/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
+++ b/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
@@ -54,7 +54,7 @@ TEST(SerializationFormatRegistryTest, isFormatRegistered) {
 
 TEST(SerializationFormatRegistryTest, EnumeratingRegistryEntries) {
   auto Formats = SerializationFormatRegistry::entries();
-  ASSERT_EQ(std::distance(Formats.begin(), Formats.end()), 1U);
+  ASSERT_EQ(std::distance(Formats.begin(), Formats.end()), 2U);
   EXPECT_EQ(Formats.begin()->getName(), "MockSerializationFormat");
 }
 
diff --git a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/JSONFormatTest.cpp b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/JSONFormatTest.cpp
index 92966f22c5971..66d112580d748 100644
--- a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/JSONFormatTest.cpp
+++ b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/JSONFormatTest.cpp
@@ -207,7 +207,7 @@ llvm::Error normalizeIDTable(json::Array &IDTable,
       return createStringError(
           inconvertibleErrorCode(),
           "Cannot normalize %s JSON: id_table entry at index %zu "
-          "does not contain a valid 'id' uint64_t field",
+          "'id' field is not a valid entity id integer",
           SummaryClassName.data(), Index);
     }
   }
@@ -246,7 +246,7 @@ llvm::Error normalizeLinkageTable(json::Array &LinkageTable,
       return createStringError(
           inconvertibleErrorCode(),
           "Cannot normalize %s JSON: linkage_table entry at index "
-          "%zu does not contain a valid 'id' uint64_t field",
+          "%zu 'id' field is not a valid entity id integer",
           SummaryClassName.data(), Index);
     }
   }
@@ -287,8 +287,8 @@ llvm::Error normalizeSummaryData(json::Array &SummaryData, size_t DataIndex,
       return createStringError(
           inconvertibleErrorCode(),
           "Cannot normalize %s JSON: data entry at index %zu, "
-          "summary_data entry at index %zu does not contain a valid "
-          "'entity_id' uint64_t field",
+          "summary_data entry at index %zu 'entity_id' field is not "
+          "a valid entity id integer",
           SummaryClassName.data(), DataIndex, SummaryIndex);
     }
   }
@@ -458,15 +458,14 @@ namespace {
 // ============================================================================
 
 json::Object serializePairsEntitySummaryForJSONFormatTest(
-    const EntitySummary &Summary,
-    const JSONFormat::EntityIdConverter &Converter) {
+    const EntitySummary &Summary, JSONFormat::EntityIdToJSONFn ToJSON) {
   const auto &TA =
       static_cast<const PairsEntitySummaryForJSONFormatTest &>(Summary);
   json::Array PairsArray;
   for (const auto &[First, Second] : TA.Pairs) {
     PairsArray.push_back(json::Object{
-        {"first", Converter.toJSON(First)},
-        {"second", Converter.toJSON(Second)},
+        {"first", ToJSON(First)},
+        {"second", ToJSON(Second)},
     });
   }
   return json::Object{{"pairs", std::move(PairsArray)}};
@@ -475,7 +474,7 @@ json::Object serializePairsEntitySummaryForJSONFormatTest(
 Expected<std::unique_ptr<EntitySummary>>
 deserializePairsEntitySummaryForJSONFormatTest(
     const json::Object &Obj, EntityIdTable &IdTable,
-    const JSONFormat::EntityIdConverter &Converter) {
+    JSONFormat::EntityIdFromJSONFn FromJSON) {
   auto Result = std::make_unique<PairsEntitySummaryForJSONFormatTest>();
   const json::Array *PairsArray = Obj.getArray("pairs");
   if (!PairsArray) {
@@ -489,20 +488,33 @@ deserializePairsEntitySummaryForJSONFormatTest(
           inconvertibleErrorCode(),
           "pairs element at index %zu is not a JSON object", Index);
     }
-    auto FirstOpt = Pair->getInteger("first");
-    if (!FirstOpt) {
+    const json::Object *FirstObj = Pair->getObject("first");
+    if (!FirstObj) {
       return createStringError(
           inconvertibleErrorCode(),
           "missing or invalid 'first' field at index '%zu'", Index);
     }
-    auto SecondOpt = Pair->getInteger("second");
-    if (!SecondOpt) {
+    const json::Object *SecondObj = Pair->getObject("second");
+    if (!SecondObj) {
       return createStringError(
           inconvertibleErrorCode(),
           "missing or invalid 'second' field at index '%zu'", Index);
     }
-    Result->Pairs.emplace_back(Converter.fromJSON(*FirstOpt),
-                               Converter.fromJSON(*SecondOpt));
+    auto ExpectedFirst = FromJSON(*FirstObj);
+    if (!ExpectedFirst) {
+      return createStringError(inconvertibleErrorCode(),
+                               "invalid 'first' entity id at index '%zu': %s",
+                               Index,
+                               toString(ExpectedFirst.takeError()).c_str());
+    }
+    auto ExpectedSecond = FromJSON(*SecondObj);
+    if (!ExpectedSecond) {
+      return createStringError(inconvertibleErrorCode(),
+                               "invalid 'second' entity id at index '%zu': %s",
+                               Index,
+                               toString(ExpectedSecond.takeError()).c_str());
+    }
+    Result->Pairs.emplace_back(*ExpectedFirst, *ExpectedSecond);
   }
   return std::move(Result);
 }
@@ -526,8 +538,9 @@ llvm::Registry<JSONFormat::FormatInfo>::Add<
 // Second Test Analysis - Simple analysis for multi-summary round-trip tests.
 // ============================================================================
 
-json::Object serializeTagsEntitySummaryForJSONFormatTest(
-    const EntitySummary &Summary, const JSONFormat::EntityIdConverter &) {
+json::Object
+serializeTagsEntitySummaryForJSONFormatTest(const EntitySummary &Summary,
+                                            JSONFormat::EntityIdToJSONFn) {
   const auto &TA =
       static_cast<const TagsEntitySummaryForJSONFormatTest &>(Summary);
   json::Array TagsArray;
@@ -538,9 +551,9 @@ json::Object serializeTagsEntitySummaryForJSONFormatTest(
 }
 
 Expected<std::unique_ptr<EntitySummary>>
-deserializeTagsEntitySummaryForJSONFormatTest(
-    const json::Object &Obj, EntityIdTable &,
-    const JSONFormat::EntityIdConverter &) {
+deserializeTagsEntitySummaryForJSONFormatTest(const json::Object &Obj,
+                                              EntityIdTable &,
+                                              JSONFormat::EntityIdFromJSONFn) {
   auto Result = std::make_unique<TagsEntitySummaryForJSONFormatTest>();
   const json::Array *TagsArray = Obj.getArray("tags");
   if (!TagsArray) {
@@ -583,10 +596,10 @@ struct NullEntitySummaryForJSONFormatTestFormatInfo final
   NullEntitySummaryForJSONFormatTestFormatInfo()
       : JSONFormat::FormatInfo(
             SummaryName("NullEntitySummaryForJSONFormatTest"),
-            [](const EntitySummary &, const JSONFormat::EntityIdConverter &)
+            [](const EntitySummary &, JSONFormat::EntityIdToJSONFn)
                 -> json::Object { return json::Object{}; },
             [](const json::Object &, EntityIdTable &,
-               const JSONFormat::EntityIdConverter &)
+               JSONFormat::EntityIdFromJSONFn)
                 -> llvm::Expected<std::unique_ptr<EntitySummary>> {
               return nullptr;
             }) {}
@@ -607,10 +620,10 @@ struct MismatchedEntitySummaryForJSONFormatTestFormatInfo final
   MismatchedEntitySummaryForJSONFormatTestFormatInfo()
       : JSONFormat::FormatInfo(
             SummaryName("MismatchedEntitySummaryForJSONFormatTest"),
-            [](const EntitySummary &, const JSONFormat::EntityIdConverter &)
+            [](const EntitySummary &, JSONFormat::EntityIdToJSONFn)
                 -> json::Object { return json::Object{}; },
             [](const json::Object &, EntityIdTable &,
-               const JSONFormat::EntityIdConverter &)
+               JSONFormat::EntityIdFromJSONFn)
                 -> llvm::Expected<std::unique_ptr<EntitySummary>> {
               return std::make_unique<
                   MismatchedEntitySummaryForJSONFormatTest>();
diff --git a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/LUSummaryTest.cpp b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/LUSummaryTest.cpp
index 5c6f32192b2c5..062bdcde86336 100644
--- a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/LUSummaryTest.cpp
+++ b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/LUSummaryTest.cpp
@@ -1340,7 +1340,7 @@ TEST_F(JSONFormatLUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "second": 1
+                  "second": {"@": 1}
                 }
               ]
             }
@@ -1383,7 +1383,7 @@ TEST_F(JSONFormatLUSummaryTest,
               "pairs": [
                 {
                   "first": "not_a_number",
-                  "second": 1
+                  "second": {"@": 1}
                 }
               ]
             }
@@ -1425,7 +1425,7 @@ TEST_F(JSONFormatLUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "first": 0
+                  "first": {"@": 0}
                 }
               ]
             }
@@ -1467,7 +1467,7 @@ TEST_F(JSONFormatLUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "first": 0,
+                  "first": {"@": 0},
                   "second": "not_a_number"
                 }
               ]
@@ -2309,7 +2309,7 @@ TEST_P(LUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 1,
             "entity_summary": {
               "pairs": [
-                { "first": 1, "second": 3 }
+                { "first": {"@": 1}, "second": {"@": 3} }
               ]
             }
           },
@@ -2317,8 +2317,8 @@ TEST_P(LUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 4,
             "entity_summary": {
               "pairs": [
-                { "first": 4, "second": 0 },
-                { "first": 4, "second": 2 }
+                { "first": {"@": 4}, "second": {"@": 0} },
+                { "first": {"@": 4}, "second": {"@": 2} }
               ]
             }
           },
@@ -2332,7 +2332,7 @@ TEST_P(LUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 3,
             "entity_summary": {
               "pairs": [
-                { "first": 3, "second": 1 }
+                { "first": {"@": 3}, "second": {"@": 1} }
               ]
             }
           },
@@ -2340,8 +2340,8 @@ TEST_P(LUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 2,
             "entity_summary": {
               "pairs": [
-                { "first": 2, "second": 4 },
-                { "first": 2, "second": 3 }
+                { "first": {"@": 2}, "second": {"@": 4} },
+                { "first": {"@": 2}, "second": {"@": 3} }
               ]
             }
           }
diff --git a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/TUSummaryTest.cpp b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/TUSummaryTest.cpp
index a8cf82806aecc..98d749d8fa67e 100644
--- a/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/TUSummaryTest.cpp
+++ b/clang/unittests/Analysis/Scalable/Serialization/JSONFormatTest/TUSummaryTest.cpp
@@ -1216,7 +1216,7 @@ TEST_F(JSONFormatTUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "second": 1
+                  "second": {"@": 1}
                 }
               ]
             }
@@ -1257,7 +1257,7 @@ TEST_F(JSONFormatTUSummaryTest,
               "pairs": [
                 {
                   "first": "not_a_number",
-                  "second": 1
+                  "second": {"@": 1}
                 }
               ]
             }
@@ -1297,7 +1297,7 @@ TEST_F(JSONFormatTUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "first": 0
+                  "first": {"@": 0}
                 }
               ]
             }
@@ -1337,7 +1337,7 @@ TEST_F(JSONFormatTUSummaryTest,
             "entity_summary": {
               "pairs": [
                 {
-                  "first": 0,
+                  "first": {"@": 0},
                   "second": "not_a_number"
                 }
               ]
@@ -2112,7 +2112,7 @@ TEST_P(TUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 1,
             "entity_summary": {
               "pairs": [
-                { "first": 1, "second": 3 }
+                { "first": {"@": 1}, "second": {"@": 3} }
               ]
             }
           },
@@ -2120,8 +2120,8 @@ TEST_P(TUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 4,
             "entity_summary": {
               "pairs": [
-                { "first": 4, "second": 0 },
-                { "first": 4, "second": 2 }
+                { "first": {"@": 4}, "second": {"@": 0} },
+                { "first": {"@": 4}, "second": {"@": 2} }
               ]
             }
           },
@@ -2135,7 +2135,7 @@ TEST_P(TUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 3,
             "entity_summary": {
               "pairs": [
-                { "first": 3, "second": 1 }
+                { "first": {"@": 3}, "second": {"@": 1} }
               ]
             }
           },
@@ -2143,8 +2143,8 @@ TEST_P(TUSummaryTest, RoundTripWithTwoSummaryTypes) {
             "entity_id": 2,
             "entity_summary": {
               "pairs": [
-                { "first": 2, "second": 4 },
-                { "first": 2, "second": 3 }
+                { "first": {"@": 2}, "second": {"@": 4} },
+                { "first": {"@": 2}, "second": {"@": 3} }
               ]
             }
           }

>From 73ac1e1255de9669452206a3d5ead84d97f41907 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 10:08:45 -0800
Subject: [PATCH 02/13] Fixes

---
 .../Scalable/Serialization/JSONFormat.h       |   6 +-
 .../JSONFormat/JSONEntitySummaryEncoding.h    |  10 +-
 .../JSONFormat/JSONFormatImpl.cpp             |  10 +-
 .../Serialization/JSONFormat/JSONFormatImpl.h |   4 -
 clang/tools/ssaf-linker/SSAFLinker.cpp        | 100 +++++++-----------
 .../SerializationFormatRegistryTest.cpp       |   7 +-
 6 files changed, 52 insertions(+), 85 deletions(-)

diff --git a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
index 60e4eeb97edb6..c918e438e426f 100644
--- a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
+++ b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
@@ -81,10 +81,10 @@ class JSONFormat final : public SerializationFormat {
   EntityId entityIdFromJSON(const uint64_t EntityIdIndex) const;
   uint64_t entityIdToJSON(EntityId EI) const;
 
-  llvm::Expected<EntityId>
-  entityIdFromJSONObject(const Object &EntityIdObject) const;
+  static llvm::Expected<EntityId>
+  entityIdFromJSONObject(const Object &EntityIdObject);
   static Value *entityIdReferenceFromJSONObject(Object &EntityIdObject);
-  Object entityIdToJSONObject(EntityId EI) const;
+  static Object entityIdToJSONObject(EntityId EI);
 
   llvm::Expected<BuildNamespace>
   buildNamespaceFromJSON(const Object &BuildNamespaceObject) const;
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
index 92fec92650c4c..aa0c6ab61f658 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
@@ -1,4 +1,4 @@
-//===- JSONEntitySummaryEncoding.h ------------------------------*- C++ -*-===//
+//===- JSONEntitySummaryEncoding.h ----------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -23,14 +23,6 @@
 
 namespace clang::ssaf {
 
-//----------------------------------------------------------------------------
-// JSONEntitySummaryEncoding
-//
-// Concrete EntitySummaryEncoding used by JSONFormat for both TUSummaryEncoding
-// and LUSummaryEncoding. Stores the raw EntitySummary JSON value opaquely so
-// the linker can patch and emit it without knowing the analysis schema.
-//----------------------------------------------------------------------------
-
 class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
   friend JSONFormat;
 
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
index 5f49ee5c10898..e4f9c3dc99452 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -149,7 +149,7 @@ uint64_t JSONFormat::entityIdToJSON(EntityId EI) const {
 }
 
 llvm::Expected<EntityId>
-JSONFormat::entityIdFromJSONObject(const Object &EntityIdObject) const {
+JSONFormat::entityIdFromJSONObject(const Object &EntityIdObject) {
   if (EntityIdObject.size() != 1) {
     return ErrorBuilder::create(std::errc::invalid_argument,
                                 ErrorMessages::FailedToReadEntityIdObject,
@@ -189,9 +189,9 @@ Value *JSONFormat::entityIdReferenceFromJSONObject(Object &EntityIdObject) {
   return AtVal;
 }
 
-Object JSONFormat::entityIdToJSONObject(EntityId EI) const {
+Object JSONFormat::entityIdToJSONObject(EntityId EI) {
   Object Result;
-  Result["@"] = static_cast<uint64_t>(getIndex(EI));
+  Result[JSONEntityIdKey] = static_cast<uint64_t>(getIndex(EI));
   return Result;
 }
 
@@ -666,7 +666,7 @@ JSONFormat::entitySummaryFromJSON(const SummaryName &SN,
 
   return InfoEntry.Deserialize(
       EntitySummaryObject, IdTable,
-      [this](const Object &Obj) { return entityIdFromJSONObject(Obj); });
+      [](const Object &Obj) { return entityIdFromJSONObject(Obj); });
 }
 
 llvm::Expected<Object>
@@ -684,7 +684,7 @@ JSONFormat::entitySummaryToJSON(const SummaryName &SN,
   assert(InfoEntry.ForSummary == SN);
 
   return InfoEntry.Serialize(
-      ES, [this](EntityId EI) { return entityIdToJSONObject(EI); });
+      ES, [](EntityId EI) { return entityIdToJSONObject(EI); });
 }
 
 //----------------------------------------------------------------------------
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
index 74837d845b8f1..a95d4c3d37db8 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.h
@@ -122,13 +122,9 @@ inline constexpr const char *FailedToPatchEntityIdNotInTable =
 // Entity Id JSON Representation
 //----------------------------------------------------------------------------
 
-namespace {
-
 /// An entity ID is encoded as the single-key object {"@": <index>}.
 inline constexpr const char *JSONEntityIdKey = "@";
 
-} // namespace
-
 //----------------------------------------------------------------------------
 // JSON Reader and Writer
 //----------------------------------------------------------------------------
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
index ad9ae56646260..d2864e543cc51 100644
--- a/clang/tools/ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -15,16 +15,18 @@
 #include "clang/Analysis/Scalable/EntityLinker/TUSummaryEncoding.h"
 #include "clang/Analysis/Scalable/Model/BuildNamespace.h"
 #include "clang/Analysis/Scalable/Serialization/JSONFormat.h"
+#include "clang/Analysis/Scalable/Serialization/SerializationFormatRegistry.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/Signals.h"
+#include "llvm/Support/Process.h"
 #include "llvm/Support/Timer.h"
 #include "llvm/Support/WithColor.h"
 #include "llvm/Support/raw_ostream.h"
-#include <map>
 #include <memory>
 #include <string>
 
@@ -42,32 +44,27 @@ namespace {
 
 cl::OptionCategory SsafLinkerCategory("ssaf-linker options");
 
-cl::list<std::string> InputPaths(cl::Positional, cl::desc("input..."),
-                                 cl::value_desc("path"), cl::OneOrMore,
-                                 cl::cat(SsafLinkerCategory));
+cl::list<std::string> InputPaths(cl::Positional, cl::desc("<input files>"),
+                                 cl::OneOrMore, cl::cat(SsafLinkerCategory));
 
-cl::opt<std::string> OutputPath("output", cl::desc("Output summary path"),
+cl::opt<std::string> OutputPath("o", cl::desc("Output summary path"),
                                 cl::value_desc("path"), cl::Required,
                                 cl::cat(SsafLinkerCategory));
 
-cl::alias OutputFileShort("o", cl::aliasopt(OutputPath));
+cl::alias OutputPathLong("output", cl::aliasopt(OutputPath));
 
 cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
                       cl::init(false), cl::cat(SsafLinkerCategory));
 
-cl::alias VerboseShort("v", cl::aliasopt(Verbose));
-
 cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
                    cl::cat(SsafLinkerCategory));
 
-cl::alias TimeShort("t", cl::aliasopt(Time));
-
 //===----------------------------------------------------------------------===//
 // Error Messages
 //===----------------------------------------------------------------------===//
 
 constexpr const char *ErrorCannotValidateSummary =
-    "Failed to validate summary '{0}': {1}";
+    "failed to validate summary '{0}': {1}.";
 
 constexpr const char *ErrorOutputDirectoryMissing =
     "Parent directory does not exist.";
@@ -80,17 +77,14 @@ constexpr const char *ErrorExtensionNotSupplied = "Extension not supplied.";
 constexpr const char *ErrorNoFormatForExtension =
     "Format not registered for extension '{0}'.";
 
-constexpr const char *ErrorCannotResolvePath =
-    "Failed to validate summary '{0}': {1}.";
-
 constexpr const char *ErrorLoadFailed =
-    "Failed to load input summary '{0}': {1}.";
+    "failed to load input summary '{0}': {1}.";
 
 constexpr const char *ErrorLinkFailed =
-    "Failed to link input summary '{0}': {1}.";
+    "failed to link input summary '{0}': {1}.";
 
 constexpr const char *ErrorWriteFailed =
-    "Failed to write output summary '{0}': {1}.";
+    "failed to write output summary '{0}': {1}.";
 
 //===----------------------------------------------------------------------===//
 // Diagnostic Utilities
@@ -100,11 +94,13 @@ constexpr unsigned IndentationWidth = 2;
 
 static llvm::StringRef ToolName;
 
+static void PrintVersion(llvm::raw_ostream &OS) { OS << "ssaf-linker 0.1\n"; }
+
 template <typename... Ts>
 [[noreturn]] void Fail(const char *Fmt, Ts &&...Args) {
   llvm::WithColor::error(llvm::errs(), ToolName)
       << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
-  std::exit(1);
+  llvm::sys::Process::Exit(1);
 }
 
 template <typename... Ts>
@@ -116,48 +112,31 @@ void Info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
   }
 }
 
-struct ScopedTimer {
-  explicit ScopedTimer(llvm::Timer &T) : T(T) {
-    if (Time) {
-      T.startTimer();
-    }
-  }
-
-  ~ScopedTimer() {
-    if (Time) {
-      T.stopTimer();
-    }
-  }
-  llvm::Timer &T;
-};
-
 //===----------------------------------------------------------------------===//
 // Format Registry
 //===----------------------------------------------------------------------===//
 
-// TODO - will be replaced by an equivalent method from the framework
-std::unique_ptr<SerializationFormat>
-MakeFormatForExtension(llvm::StringRef Extension) {
-  if (Extension == "json") {
-    return std::make_unique<JSONFormat>();
-  }
-  return nullptr;
-}
-
 SerializationFormat *GetFormatForExtension(llvm::StringRef Extension) {
-  static std::map<std::string, std::unique_ptr<SerializationFormat>>
-      ExtensionFormatMap;
-
-  auto It = ExtensionFormatMap.find(Extension.str());
-  if (It != ExtensionFormatMap.end()) {
+  static llvm::SmallVector<
+      std::pair<std::string, std::unique_ptr<SerializationFormat>>, 4>
+      ExtensionFormatList;
+
+  // Most recently used format is most likely to be reused again.
+  auto ReversedList = llvm::reverse(ExtensionFormatList);
+  auto It = llvm::find_if(ReversedList, [&](const auto &Entry) {
+    return Entry.first == Extension;
+  });
+  if (It != ReversedList.end()) {
     return It->second.get();
   }
 
-  auto Format = MakeFormatForExtension(Extension);
+  // SerializationFormats are uppercase while file extensions are lowercase.
+  std::string CapitalizedExtension = Extension.upper();
+  auto Format = makeFormat(CapitalizedExtension);
   SerializationFormat *Result = Format.get();
 
   if (Result) {
-    ExtensionFormatMap.emplace(Extension, std::move(Format));
+    ExtensionFormatList.emplace_back(Extension, std::move(Format));
   }
 
   return Result;
@@ -201,7 +180,7 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
   LinkerInput LI;
 
   {
-    ScopedTimer _(TValidate);
+    llvm::TimeRegion _(Time ? &TValidate : nullptr);
     llvm::StringRef ParentDir = path::parent_path(OutputPath);
     llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
 
@@ -221,12 +200,12 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
   Info(2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
 
   {
-    ScopedTimer _(TValidate);
+    llvm::TimeRegion _(Time ? &TValidate : nullptr);
     for (const auto &InputPath : InputPaths) {
       llvm::SmallString<256> RealPath;
       std::error_code EC = fs::real_path(InputPath, RealPath, true);
       if (EC) {
-        Fail(ErrorCannotResolvePath, InputPath, EC.message());
+        Fail(ErrorCannotValidateSummary, InputPath, EC.message());
       }
       LI.InputFiles.push_back(SummaryFile::FromPath(RealPath));
     }
@@ -256,7 +235,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
       Info(3, "[{0}/{1}] Reading '{2}'.", (Index + 1), LI.InputFiles.size(),
            InputFile.Path);
 
-      ScopedTimer _(TRead);
+      llvm::TimeRegion _(Time ? &TRead : nullptr);
 
       auto ExpectedSummaryEncoding =
           InputFile.Format->readTUSummaryEncoding(InputFile.Path);
@@ -273,7 +252,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
       Info(3, "[{0}/{1}] Linking '{2}'.", (Index + 1), LI.InputFiles.size(),
            InputFile.Path);
 
-      ScopedTimer _(TLink);
+      llvm::TimeRegion _(Time ? &TLink : nullptr);
 
       if (auto Err = EL.link(std::move(Summary))) {
         Fail(ErrorLinkFailed, InputFile.Path, toString(std::move(Err)));
@@ -284,7 +263,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
   {
     Info(2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
 
-    ScopedTimer _(TWrite);
+    llvm::TimeRegion _(Time ? &TWrite : nullptr);
 
     auto Output = std::move(EL).getOutput();
     if (auto Err = LI.OutputFile.Format->writeLUSummaryEncoding(
@@ -301,11 +280,12 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
 //===----------------------------------------------------------------------===//
 
 int main(int argc, const char **argv) {
-  ToolName = argv[0];
+  InitLLVM X(argc, argv);
+  ToolName = llvm::sys::path::filename(argv[0]);
   initializeJSONFormat();
-  sys::PrintStackTraceOnErrorSignal(argv[0]);
 
   cl::HideUnrelatedOptions(SsafLinkerCategory);
+  cl::SetVersionPrinter(PrintVersion);
   cl::ParseCommandLineOptions(argc, argv, "SSAF Linker\n");
 
   llvm::TimerGroup LinkerTimers("ssaf-linker", "SSAF Linker");
@@ -327,9 +307,5 @@ int main(int argc, const char **argv) {
     Info(0, "Linking finished.");
   }
 
-  if (Time) {
-    LinkerTimers.print(llvm::errs());
-  }
-
   return 0;
 }
diff --git a/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp b/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
index b8f1ea0a866e4..65e01cca4ce97 100644
--- a/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
+++ b/clang/unittests/Analysis/Scalable/Registries/SerializationFormatRegistryTest.cpp
@@ -8,6 +8,7 @@
 
 #include "clang/Analysis/Scalable/Serialization/SerializationFormatRegistry.h"
 #include "clang/Analysis/Scalable/TUSummary/TUSummary.h"
+#include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/ScopeExit.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/FileSystem.h"
@@ -54,8 +55,10 @@ TEST(SerializationFormatRegistryTest, isFormatRegistered) {
 
 TEST(SerializationFormatRegistryTest, EnumeratingRegistryEntries) {
   auto Formats = SerializationFormatRegistry::entries();
-  ASSERT_EQ(std::distance(Formats.begin(), Formats.end()), 2U);
-  EXPECT_EQ(Formats.begin()->getName(), "MockSerializationFormat");
+  ASSERT_GE(std::distance(Formats.begin(), Formats.end()), 1U);
+  EXPECT_TRUE(llvm::any_of(Formats, [](const auto &Entry) {
+    return StringRef(Entry.getName()) == "MockSerializationFormat";
+  }));
 }
 
 TEST(SerializationFormatRegistryTest, Roundtrip) {

>From 45ccc0b80dc71eb922fe526496e8d4111a0a0a34 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 12:01:26 -0800
Subject: [PATCH 03/13] Fix basic tests

---
 .../Analysis/Scalable/ssaf-linker/help.test   | 10 ++--
 .../Analysis/Scalable/ssaf-linker/time.test   | 37 +++++----------
 .../ssaf-linker/validation-errors.test        | 16 +++----
 .../Scalable/ssaf-linker/verbose.test         | 46 ++++++-------------
 .../Scalable/ssaf-linker/version.test         |  5 ++
 clang/tools/ssaf-linker/SSAFLinker.cpp        | 10 ++--
 6 files changed, 49 insertions(+), 75 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/version.test

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index bc90d79435bc7..900f2285c5d5d 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -4,7 +4,7 @@
 
 // CHECK: OVERVIEW: SSAF Linker
 // CHECK-EMPTY:
-// CHECK: USAGE: ssaf-linker [options] input...
+// CHECK: USAGE: ssaf-linker [options] <input files>
 // CHECK-EMPTY:
 // CHECK: OPTIONS:
 // CHECK:   -h                  - Alias for --help
@@ -12,12 +12,10 @@
 // CHECK:   --help-hidden       - Display all available options
 // CHECK:   --help-list         - Display list of available options (--help-list-hidden for more)
 // CHECK:   --help-list-hidden  - Display list of all available options
-// CHECK:   -o                  -
-// CHECK:   --output=<path>     - Output summary path
+// CHECK:   -o <path>           - Output summary path
+// CHECK:   --output            -
 // CHECK:   --print-all-options - Print all option values after command line parsing
 // CHECK:   --print-options     - Print non-default options after command line parsing
-// CHECK:   -t                  -
 // CHECK:   --time              - Enable timing
-// CHECK:   -v                  -
 // CHECK:   --verbose           - Enable verbose output
-// CHECK:   --version           - Display the version of this program
+// CHECK:   --version           - Display the version of this program
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/time.test b/clang/test/Analysis/Scalable/ssaf-linker/time.test
index 0f7033876313f..8737c6d9e0eef 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/time.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/time.test
@@ -4,29 +4,14 @@
 // RUN: mkdir -p %t
 
 // RUN: ssaf-linker --time %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s --check-prefix=CHECK-LONG
-// CHECK-LONG: ===-------------------------------------------------------------------------===
-// CHECK-LONG:                                   SSAF Linker
-// CHECK-LONG: ===-------------------------------------------------------------------------===
-// CHECK-LONG: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
-// CHECK-LONG: ---User Time---   --System Time--   --User+System--   ---Wall Time---
-// CHECK-LONG-DAG: {{.*}}Write Summary
-// CHECK-LONG-DAG: {{.*}}Read Summaries
-// CHECK-LONG-DAG: {{.*}}Link Summaries
-// CHECK-LONG-DAG: {{.*}}Validate Input
-// CHECK-LONG: {{.*}}Total
-
-// RUN: rm %t/lu-1+2.json
-
-// RUN: ssaf-linker -t %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s --check-prefix=CHECK-SHORT
-// CHECK-SHORT: ===-------------------------------------------------------------------------===
-// CHECK-SHORT:                                   SSAF Linker
-// CHECK-SHORT: ===-------------------------------------------------------------------------===
-// CHECK-SHORT: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
-// CHECK-SHORT: ---User Time---   --System Time--   --User+System--   ---Wall Time---
-// CHECK-SHORT-DAG: {{.*}}Write Summary
-// CHECK-SHORT-DAG: {{.*}}Read Summaries
-// CHECK-SHORT-DAG: {{.*}}Link Summaries
-// CHECK-SHORT-DAG: {{.*}}Validate Input
-// CHECK-SHORT: {{.*}}Total
\ No newline at end of file
+// RUN:           | FileCheck %s
+// CHECK: ===-------------------------------------------------------------------------===
+// CHECK:                                   SSAF Linker
+// CHECK: ===-------------------------------------------------------------------------===
+// CHECK: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
+// CHECK: ---User Time---   --System Time--   --User+System--   ---Wall Time---
+// CHECK-DAG: {{.*}}Write Summary
+// CHECK-DAG: {{.*}}Read Summaries
+// CHECK-DAG: {{.*}}Link Summaries
+// CHECK-DAG: {{.*}}Validate Input
+// CHECK: {{.*}}Total
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index 1fc99a18e8041..2a8476f8be48b 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -5,27 +5,27 @@
 // No extension on output.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-EXT-OUTPUT
-// NO-EXT-OUTPUT: ssaf-linker: error: Failed to validate summary 'lu': Extension not supplied.
+// NO-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu': Extension not supplied.
 
 // No extension on input.
 // RUN: not ssaf-linker %S/Inputs/tu-noext -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-EXT-INPUT -DPATH=%S/Inputs/tu-noext
-// NO-EXT-INPUT: ssaf-linker: error: Failed to validate summary '[[PATH]]': Extension not supplied.
+// NO-EXT-INPUT: ssaf-linker: error: failed to validate summary '[[PATH]]': Extension not supplied.
 
 // Invalid extension on output.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu.txt 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=BAD-EXT-OUTPUT
-// BAD-EXT-OUTPUT: ssaf-linker: error: Failed to validate summary 'lu.txt': Format not registered for extension 'txt'.
+// BAD-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu.txt': Format not registered for extension 'txt'.
 
 // Invalid extension on input.
 // RUN: not ssaf-linker %S/Inputs/tu-badext.txt -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=BAD-EXT-INPUT -DPATH=%S/Inputs/tu-badext.txt
-// BAD-EXT-INPUT: ssaf-linker: error: Failed to validate summary '[[PATH]]': Format not registered for extension 'txt'.
+// BAD-EXT-INPUT: ssaf-linker: error: failed to validate summary '[[PATH]]': Format not registered for extension 'txt'.
 
 // Output directory does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %S/Outputs/NonExistentDirectory/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=OUTPUT-PARENT-DIR-MISSING -DPATH=%S/Outputs/NonExistentDirectory/lu.json
-// OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: Failed to validate summary '[[PATH]]': Parent directory does not exist.
+// OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: failed to validate summary '[[PATH]]': Parent directory does not exist.
 
 // Output parent directory exists but is not writable.
 // UNSUPPORTED: system-windows
@@ -34,16 +34,16 @@
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/output-dir/output.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-WRITE-PERM -DPATH=%t/output-dir/output.json
 // RUN: chmod +w %t/output-dir
-// NO-WRITE-PERM: ssaf-linker: error: Failed to validate summary '[[PATH]]': Parent directory is not writable.
+// NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '[[PATH]]': Parent directory is not writable.
 
 // Input summary does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-nonexistent.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=NO-INPUT-FILE -DPATH=%S/Inputs/tu-nonexistent.json
-// NO-INPUT-FILE: ssaf-linker: error: Failed to validate summary '[[PATH]]': No such file or directory.
+// NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '[[PATH]]': No such file or directory.
 
 // Input summary is a broken symlink.
 // RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
 // RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=BROKEN-SYMLINK -DPATH=%t/tu-dangling.json
-// BROKEN-SYMLINK: ssaf-linker: error: Failed to validate summary '[[PATH]]': No such file or directory.
+// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '[[PATH]]': No such file or directory.
 
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
index 81c8c922e5324..0f7777043d1d9 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
@@ -4,35 +4,17 @@
 // RUN: mkdir -p %t
 
 // RUN: ssaf-linker --verbose %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s --check-prefix=CHECK-LONG -DINPUT=%S/Inputs -DOUTPUT=%t
-// CHECK-LONG: note: - Linking started.
-// CHECK-LONG: note:   - Validating input.
-// CHECK-LONG: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
-// CHECK-LONG: note:     - Validated 2 input summary paths.
-// CHECK-LONG: note:   - Linking input.
-// CHECK-LONG: note:     - Constructing linker.
-// CHECK-LONG: note:     - Linking summaries.
-// CHECK-LONG: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
-// CHECK-LONG: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
-// CHECK-LONG: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
-// CHECK-LONG: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
-// CHECK-LONG: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
-// CHECK-LONG: note: - Linking finished.
-
-// RUN: rm %t/lu-1+2.json
-
-// RUN: ssaf-linker -v %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s --check-prefix=CHECK-SHORT -DINPUT=%S/Inputs -DOUTPUT=%t
-// CHECK-SHORT: note: - Linking started.
-// CHECK-SHORT: note:   - Validating input.
-// CHECK-SHORT: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
-// CHECK-SHORT: note:     - Validated 2 input summary paths.
-// CHECK-SHORT: note:   - Linking input.
-// CHECK-SHORT: note:     - Constructing linker.
-// CHECK-SHORT: note:     - Linking summaries.
-// CHECK-SHORT: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
-// CHECK-SHORT: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
-// CHECK-SHORT: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
-// CHECK-SHORT: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
-// CHECK-SHORT: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
-// CHECK-SHORT: note: - Linking finished.
\ No newline at end of file
+// RUN:           | FileCheck %s -DINPUT=%S/Inputs -DOUTPUT=%t
+// CHECK: note: - Linking started.
+// CHECK: note:   - Validating input.
+// CHECK: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
+// CHECK: note:     - Validated 2 input summary paths.
+// CHECK: note:   - Linking input.
+// CHECK: note:     - Constructing linker.
+// CHECK: note:     - Linking summaries.
+// CHECK: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
+// CHECK: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
+// CHECK: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
+// CHECK: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
+// CHECK: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
+// CHECK: note: - Linking finished.
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/version.test b/clang/test/Analysis/Scalable/ssaf-linker/version.test
new file mode 100644
index 0000000000000..e68e4eeb47152
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/version.test
@@ -0,0 +1,5 @@
+// Test ssaf-linker version
+
+// RUN: ssaf-linker --version | FileCheck %s
+
+// CHECK: ssaf-linker 0.1
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
index d2864e543cc51..d3485508c1f74 100644
--- a/clang/tools/ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -132,12 +132,16 @@ SerializationFormat *GetFormatForExtension(llvm::StringRef Extension) {
 
   // SerializationFormats are uppercase while file extensions are lowercase.
   std::string CapitalizedExtension = Extension.upper();
+
+  if (!isFormatRegistered(CapitalizedExtension)) {
+    return nullptr;
+  }
+
   auto Format = makeFormat(CapitalizedExtension);
   SerializationFormat *Result = Format.get();
+  assert(Result);
 
-  if (Result) {
-    ExtensionFormatList.emplace_back(Extension, std::move(Format));
-  }
+  ExtensionFormatList.emplace_back(Extension, std::move(Format));
 
   return Result;
 }

>From d78ef5cdf1d59057165715626d3fec0b9c16b22a Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 15:31:11 -0800
Subject: [PATCH 04/13] More fixes

---
 .../Scalable/EntityLinker/EntityLinker.h      | 11 +--
 .../Scalable/Serialization/JSONFormat.h       |  1 -
 .../Analysis/Scalable/Support/ErrorBuilder.h  |  5 +-
 .../Scalable/EntityLinker/EntityLinker.cpp    | 16 ++---
 .../JSONFormat/JSONEntitySummaryEncoding.cpp  | 62 +++++++++++-----
 .../JSONFormat/JSONEntitySummaryEncoding.h    | 11 +--
 .../JSONFormat/JSONFormatImpl.cpp             | 13 ----
 .../Inputs/tu-invalid-entity-id-multikey.json | 32 +++++++++
 .../Inputs/tu-invalid-entity-id-ref.json      | 32 +++++++++
 .../Inputs/tu-invalid-entity-id-value.json    | 32 +++++++++
 .../ssaf-linker/Inputs/tu-missing-fields.json |  6 ++
 .../Analysis/Scalable/ssaf-linker/cli.test    |  2 +-
 .../Analysis/Scalable/ssaf-linker/help.test   |  1 -
 .../Analysis/Scalable/ssaf-linker/io.test     | 34 +++++----
 .../Scalable/ssaf-linker/linking-errors.test  | 21 +++++-
 .../ssaf-linker/validation-errors.test        |  3 +-
 clang/tools/ssaf-linker/SSAFLinker.cpp        | 70 +++++++++++--------
 17 files changed, 249 insertions(+), 103 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-missing-fields.json

diff --git a/clang/include/clang/Analysis/Scalable/EntityLinker/EntityLinker.h b/clang/include/clang/Analysis/Scalable/EntityLinker/EntityLinker.h
index c17e6cdc2955d..28d70e9e5f1fd 100644
--- a/clang/include/clang/Analysis/Scalable/EntityLinker/EntityLinker.h
+++ b/clang/include/clang/Analysis/Scalable/EntityLinker/EntityLinker.h
@@ -42,9 +42,9 @@ class EntityLinker {
   /// and merges them into a single data store.
   ///
   /// \param Summary The TU summary to link. Ownership is transferred.
-  /// \returns Error if the TU namespace has already been linked, success
-  ///          otherwise. Corrupted summary data (missing linkage information,
-  ///          duplicate entity IDs, etc.) triggers a fatal error.
+  /// \returns Error if the TU namespace has already been linked or if patching
+  ///          fails, success otherwise. Corrupted summary data (missing linkage
+  ///          information, duplicate entity IDs, etc.) triggers a fatal error.
   llvm::Error link(std::unique_ptr<TUSummaryEncoding> Summary);
 
   /// Returns the accumulated LU summary.
@@ -81,8 +81,9 @@ class EntityLinker {
   ///
   /// \param PatchTargets Vector of summary encodings that need patching.
   /// \param EntityResolutionTable Map from TU EntityIds to LU EntityIds.
-  void patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
-             const std::map<EntityId, EntityId> &EntityResolutionTable);
+  /// \returns Error if patching any encoding fails, success otherwise.
+  llvm::Error patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
+                    const std::map<EntityId, EntityId> &EntityResolutionTable);
 };
 
 } // namespace clang::ssaf
diff --git a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
index c918e438e426f..8c7cbd39e7099 100644
--- a/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
+++ b/clang/include/clang/Analysis/Scalable/Serialization/JSONFormat.h
@@ -83,7 +83,6 @@ class JSONFormat final : public SerializationFormat {
 
   static llvm::Expected<EntityId>
   entityIdFromJSONObject(const Object &EntityIdObject);
-  static Value *entityIdReferenceFromJSONObject(Object &EntityIdObject);
   static Object entityIdToJSONObject(EntityId EI);
 
   llvm::Expected<BuildNamespace>
diff --git a/clang/include/clang/Analysis/Scalable/Support/ErrorBuilder.h b/clang/include/clang/Analysis/Scalable/Support/ErrorBuilder.h
index 81c3991c315e8..a22936663fa39 100644
--- a/clang/include/clang/Analysis/Scalable/Support/ErrorBuilder.h
+++ b/clang/include/clang/Analysis/Scalable/Support/ErrorBuilder.h
@@ -199,8 +199,9 @@ class ErrorBuilder {
   /// \endcode
   template <typename... Args>
   [[noreturn]] static void fatal(const char *Fmt, Args &&...ArgVals) {
-    llvm::report_fatal_error(llvm::StringRef(
-        formatErrorMessage(Fmt, std::forward<Args>(ArgVals)...)));
+    llvm::report_fatal_error(llvm::StringRef(formatErrorMessage(
+                                 Fmt, std::forward<Args>(ArgVals)...)),
+                             false);
   }
 };
 
diff --git a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
index 5449dada64c68..2cb23718d0fe8 100644
--- a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
+++ b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
@@ -161,19 +161,17 @@ EntityLinker::merge(TUSummaryEncoding &Summary,
   return PatchTargets;
 }
 
-void EntityLinker::patch(
-    const std::vector<EntitySummaryEncoding *> &PatchTargets,
-    const std::map<EntityId, EntityId> &EntityResolutionTable) {
+llvm::Error
+EntityLinker::patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
+                    const std::map<EntityId, EntityId> &EntityResolutionTable) {
   for (auto *PatchTarget : PatchTargets) {
     assert(PatchTarget && "EntityLinker::patch: Patch target cannot be null");
 
     if (auto Err = PatchTarget->patch(EntityResolutionTable)) {
-      std::string PatchingErrorMessage = llvm::toString(std::move(Err));
-      ErrorBuilder::fatal("{0} - {1}",
-                          ErrorMessages::EntityLinkerFatalErrorPrefix,
-                          PatchingErrorMessage);
+      return Err;
     }
   }
+  return llvm::Error::success();
 }
 
 llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
@@ -189,7 +187,9 @@ llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
 
   auto EntityResolutionTable = resolve(SummaryRef);
   auto PatchTargets = merge(SummaryRef, EntityResolutionTable);
-  patch(PatchTargets, EntityResolutionTable);
+  if (auto Err = patch(PatchTargets, EntityResolutionTable)) {
+    return Err;
+  }
 
   return llvm::Error::success();
 }
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
index 13a20ed1800a4..6cab6885db051 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
@@ -11,29 +11,55 @@
 
 namespace clang::ssaf {
 
+llvm::Expected<bool> JSONEntitySummaryEncoding::patchEntityIdObject(
+    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
+
+  llvm::json::Value *AtVal = Obj.get(JSONEntityIdKey);
+  if (!AtVal) {
+    return false;
+  }
+
+  if (Obj.size() != 1) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadEntityIdObject,
+                                JSONEntityIdKey)
+        .build();
+  }
+
+  std::optional<uint64_t> OptEntityIdIndex = AtVal->getAsUINT64();
+  if (!OptEntityIdIndex) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToReadEntityIdObject,
+                                JSONEntityIdKey)
+        .build();
+  }
+
+  auto OldId = JSONFormat::makeEntityId(*OptEntityIdIndex);
+  auto It = Table.find(OldId);
+  if (It == Table.end()) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::FailedToPatchEntityIdNotInTable,
+                                OldId)
+        .build();
+  }
+
+  *AtVal = static_cast<uint64_t>(JSONFormat::getIndex(It->second));
+
+  return true;
+}
+
 llvm::Error JSONEntitySummaryEncoding::patchObject(
     llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
 
-  if (auto AtVal = JSONFormat::entityIdReferenceFromJSONObject(Obj)) {
-    std::optional<uint64_t> OptEntityIdIndex = AtVal->getAsUINT64();
-    if (!OptEntityIdIndex) {
-      return ErrorBuilder::create(std::errc::invalid_argument,
-                                  ErrorMessages::FailedToReadEntityIdObject,
-                                  JSONEntityIdKey)
-          .build();
-    }
+  auto ExpectedIsEntityId = patchEntityIdObject(Obj, Table);
 
-    auto OldId = JSONFormat::makeEntityId(*OptEntityIdIndex);
-    auto It = Table.find(OldId);
-    if (It == Table.end()) {
-      return ErrorBuilder::create(
-                 std::errc::invalid_argument,
-                 ErrorMessages::FailedToPatchEntityIdNotInTable, OldId)
-          .build();
-    }
+  if (!ExpectedIsEntityId) {
+    return ExpectedIsEntityId.takeError();
+  }
+
+  bool IsEntityId = *ExpectedIsEntityId;
 
-    *AtVal = static_cast<uint64_t>(JSONFormat::getIndex(It->second));
-  } else {
+  if (!IsEntityId) {
     for (auto &[Key, Val] : Obj) {
       if (auto Err = patchValue(Val, Table)) {
         return Err;
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
index aa0c6ab61f658..aa826671e2706 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
@@ -34,10 +34,13 @@ class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
   explicit JSONEntitySummaryEncoding(llvm::json::Value Data)
       : Data(std::move(Data)) {}
 
-  static llvm::Error patchObject(llvm::json::Object &Obj,
-                                 const std::map<EntityId, EntityId> &Table);
-  static llvm::Error patchValue(llvm::json::Value &V,
-                                const std::map<EntityId, EntityId> &Table);
+  llvm::Expected<bool>
+  patchEntityIdObject(llvm::json::Object &Obj,
+                      const std::map<EntityId, EntityId> &Table);
+  llvm::Error patchObject(llvm::json::Object &Obj,
+                          const std::map<EntityId, EntityId> &Table);
+  llvm::Error patchValue(llvm::json::Value &V,
+                         const std::map<EntityId, EntityId> &Table);
 
   llvm::json::Value Data;
 };
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
index e4f9c3dc99452..dc5831d89a147 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -176,19 +176,6 @@ JSONFormat::entityIdFromJSONObject(const Object &EntityIdObject) {
   return makeEntityId(static_cast<size_t>(*OptEntityIdIndex));
 }
 
-Value *JSONFormat::entityIdReferenceFromJSONObject(Object &EntityIdObject) {
-  if (EntityIdObject.size() != 1) {
-    return nullptr;
-  }
-
-  llvm::json::Value *AtVal = EntityIdObject.get(JSONEntityIdKey);
-  if (!AtVal) {
-    return nullptr;
-  }
-
-  return AtVal;
-}
-
 Object JSONFormat::entityIdToJSONObject(EntityId EI) {
   Object Result;
   Result[JSONEntityIdKey] = static_cast<uint64_t>(getIndex(EI));
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
new file mode 100644
index 0000000000000..ef05419fb2b0b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
@@ -0,0 +1,32 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "invalid-entity-id-multikey.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-multikey.cpp"}],
+        "suffix": "",
+        "usr": "c:@F at foo#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {"id": 0, "linkage": {"type": "External"}}
+  ],
+  "data": [
+    {
+      "summary_name": "TestAnalysis",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "ref": {"@": 0, "extra": "field"}
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
new file mode 100644
index 0000000000000..02b8315bd1679
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
@@ -0,0 +1,32 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "invalid-entity-id-ref.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-ref.cpp"}],
+        "suffix": "",
+        "usr": "c:@F at foo#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {"id": 0, "linkage": {"type": "External"}}
+  ],
+  "data": [
+    {
+      "summary_name": "TestAnalysis",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "ref": {"@": 99}
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
new file mode 100644
index 0000000000000..bf89df75671b3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
@@ -0,0 +1,32 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "invalid-entity-id-value.cpp"
+  },
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-value.cpp"}],
+        "suffix": "",
+        "usr": "c:@F at foo#"
+      }
+    }
+  ],
+  "linkage_table": [
+    {"id": 0, "linkage": {"type": "External"}}
+  ],
+  "data": [
+    {
+      "summary_name": "TestAnalysis",
+      "summary_data": [
+        {
+          "entity_id": 0,
+          "entity_summary": {
+            "ref": {"@": "not-a-number"}
+          }
+        }
+      ]
+    }
+  ]
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-missing-fields.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-missing-fields.json
new file mode 100644
index 0000000000000..80256d6fd47a8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-missing-fields.json
@@ -0,0 +1,6 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "missing-fields.cpp"
+  }
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/cli.test b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
index 87302e78187b8..337843562e74b 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/cli.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
@@ -5,7 +5,7 @@
 // NO-ARGS: Must specify at least 1 positional argument: See: ssaf-linker --help
 
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 | FileCheck %s --check-prefix=NO-OUTPUT
-// NO-OUTPUT: ssaf-linker: for the --output option: must be specified at least once!
+// NO-OUTPUT: ssaf-linker: for the -o option: must be specified at least once!
 
 // RUN: not ssaf-linker -o %t/output.json 2>&1 | FileCheck %s --check-prefix=NO-INPUT
 // NO-INPUT: ssaf-linker: Not enough positional command line arguments specified!
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index 900f2285c5d5d..d239fba48092a 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -13,7 +13,6 @@
 // CHECK:   --help-list         - Display list of available options (--help-list-hidden for more)
 // CHECK:   --help-list-hidden  - Display list of all available options
 // CHECK:   -o <path>           - Output summary path
-// CHECK:   --output            -
 // CHECK:   --print-all-options - Print all option values after command line parsing
 // CHECK:   --print-options     - Print non-default options after command line parsing
 // CHECK:   --time              - Enable timing
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/io.test b/clang/test/Analysis/Scalable/ssaf-linker/io.test
index 386e64602c251..96ff839c362e7 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/io.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/io.test
@@ -1,26 +1,24 @@
 // Tests for ssaf-linker file I/O error handling.
 
-// Input file is a directory.
-// RUN: not ssaf-linker %S/Inputs -o %t.json 2>&1 | FileCheck %s --check-prefix=INPUT-IS-DIR
-// INPUT-IS-DIR: error: Failed to load input summary
-// INPUT-IS-DIR: path is a directory, not a file
-
-// Input file has wrong extension — caught at validation before read.
-// (Extension check happens in SummaryFile::FromPath before real_path.)
-// RUN: not ssaf-linker %S/Inputs/empty.json %S/Inputs/malformed.json -o %t.json 2>&1
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
 
 // Malformed JSON input.
-// RUN: not ssaf-linker %S/Inputs/malformed.json -o %t.json 2>&1 | FileCheck %s --check-prefix=BAD-JSON
-// BAD-JSON: error: Failed to load input summary
-// BAD-JSON: failed to read file
+// RUN: not ssaf-linker %S/Inputs/tu-malformed.json -o %t/out.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-JSON -DPATH=%S/Inputs/tu-malformed.json
+// BAD-JSON: ssaf-linker: error: reading TUSummary from file '[[PATH]]'
+// BAD-JSON: Invalid JSON value
 
 // Missing required fields in otherwise valid JSON.
-// RUN: not ssaf-linker %S/Inputs/missing-fields.json -o %t.json 2>&1 | FileCheck %s --check-prefix=MISSING-FIELDS
-// MISSING-FIELDS: error: Failed to load input summary
-// MISSING-FIELDS: failed to read
+// RUN: not ssaf-linker %S/Inputs/tu-missing-fields.json -o %t/out.json 2>&1 \
+//        | FileCheck %s --check-prefix=MISSING-FIELDS -DPATH=%S/Inputs/tu-missing-fields.json
+// MISSING-FIELDS: ssaf-linker: error: reading TUSummary from file '[[PATH]]'
+// MISSING-FIELDS: failed to read IdTable from field 'id_table': expected JSON array
 
 // Output file already exists.
-// RUN: ssaf-linker %S/Inputs/empty.json -o %t-exists.json
-// RUN: not ssaf-linker %S/Inputs/empty.json -o %t-exists.json 2>&1 | FileCheck %s --check-prefix=OUTPUT-EXISTS
-// OUTPUT-EXISTS: error: Failed to write output summary
-// OUTPUT-EXISTS: file already exists
+// RUN: touch %t/out.json
+// RUN: ssaf-linker %S/Inputs/tu-empty.json -o %t/out.json
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/out.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUTPUT-EXISTS -DPATH=%t/out.json
+// OUTPUT-EXISTS: ssaf-linker: error: writing LUSummary to file '[[PATH]]'
+// OUTPUT-EXISTS: failed to write file '[[PATH]]': file already exists
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
index 19b08936cef3d..13b9f471df7f8 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
@@ -6,4 +6,23 @@
 // Linking the same TU namespace twice produces an error.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json %S/Inputs/tu-empty.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --check-prefix=DUP-NS -DPATH=%S/Inputs/tu-empty.json
-// DUP-NS: ssaf-linker: error: Failed to link input summary '[[PATH]]': failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp).
+// DUP-NS: ssaf-linker: error: Linking summary '[[PATH]]'
+// DUP-NS: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp)
+
+// Entity ID object in summary data blob with '@' key alongside extra keys is a fatal error.
+// RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-multikey.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-ID-MULTIKEY -DPATH=%S/Inputs/tu-invalid-entity-id-multikey.json
+// INVALID-ID-MULTIKEY: ssaf-linker: error: Linking summary '[[PATH]]'
+// INVALID-ID-MULTIKEY: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
+
+// Entity ID object in summary data blob with a non-uint64 '@' value is a fatal error.
+// RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-value.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-ID-VALUE -DPATH=%S/Inputs/tu-invalid-entity-id-value.json
+// INVALID-ID-VALUE: ssaf-linker: error: Linking summary '[[PATH]]'
+// INVALID-ID-VALUE: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
+
+// Entity ID reference in summary data blob pointing to an ID absent from the resolution table
+// RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-ref.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-ID-REF -DPATH=%S/Inputs/tu-invalid-entity-id-ref.json
+// INVALID-ID-REF: ssaf-linker: error: Linking summary '[[PATH]]'
+// INVALID-ID-REF: failed to patch EntityId: 'EntityId(99)' not found in entity resolution table
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index 2a8476f8be48b..50709846aebe9 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -1,5 +1,6 @@
-// Tests for EntityLinker error cases.
+// Tests for ssaf-linker input validation.
 
+// RUN: rm -rf %t
 // RUN: mkdir -p %t
 
 // No extension on output.
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
index d3485508c1f74..2be482859a3b1 100644
--- a/clang/tools/ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -16,6 +16,7 @@
 #include "clang/Analysis/Scalable/Model/BuildNamespace.h"
 #include "clang/Analysis/Scalable/Serialization/JSONFormat.h"
 #include "clang/Analysis/Scalable/Serialization/SerializationFormatRegistry.h"
+#include "clang/Analysis/Scalable/Support/ErrorBuilder.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/CommandLine.h"
@@ -29,6 +30,7 @@
 #include "llvm/Support/raw_ostream.h"
 #include <memory>
 #include <string>
+#include <system_error>
 
 using namespace llvm;
 using namespace clang::ssaf;
@@ -51,8 +53,6 @@ cl::opt<std::string> OutputPath("o", cl::desc("Output summary path"),
                                 cl::value_desc("path"), cl::Required,
                                 cl::cat(SsafLinkerCategory));
 
-cl::alias OutputPathLong("output", cl::aliasopt(OutputPath));
-
 cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
                       cl::init(false), cl::cat(SsafLinkerCategory));
 
@@ -63,28 +63,25 @@ cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
 // Error Messages
 //===----------------------------------------------------------------------===//
 
-constexpr const char *ErrorCannotValidateSummary =
+namespace ErrorMessages {
+
+constexpr const char *CannotValidateSummary =
     "failed to validate summary '{0}': {1}.";
 
-constexpr const char *ErrorOutputDirectoryMissing =
+constexpr const char *OutputDirectoryMissing =
     "Parent directory does not exist.";
 
-constexpr const char *ErrorOutputDirectoryNotWritable =
+constexpr const char *OutputDirectoryNotWritable =
     "Parent directory is not writable.";
 
-constexpr const char *ErrorExtensionNotSupplied = "Extension not supplied.";
+constexpr const char *ExtensionNotSupplied = "Extension not supplied.";
 
-constexpr const char *ErrorNoFormatForExtension =
+constexpr const char *NoFormatForExtension =
     "Format not registered for extension '{0}'.";
 
-constexpr const char *ErrorLoadFailed =
-    "failed to load input summary '{0}': {1}.";
-
-constexpr const char *ErrorLinkFailed =
-    "failed to link input summary '{0}': {1}.";
+constexpr const char *LinkingSummary = "Linking summary '{0}'";
 
-constexpr const char *ErrorWriteFailed =
-    "failed to write output summary '{0}': {1}.";
+} // namespace ErrorMessages
 
 //===----------------------------------------------------------------------===//
 // Diagnostic Utilities
@@ -92,15 +89,22 @@ constexpr const char *ErrorWriteFailed =
 
 constexpr unsigned IndentationWidth = 2;
 
-static llvm::StringRef ToolName;
+llvm::StringRef ToolName;
 
-static void PrintVersion(llvm::raw_ostream &OS) { OS << "ssaf-linker 0.1\n"; }
+template <typename... Ts> [[noreturn]] void Fail(const char *Msg) {
+  llvm::WithColor::error(llvm::errs(), ToolName) << Msg << "\n";
+  llvm::sys::Process::Exit(1);
+}
 
 template <typename... Ts>
 [[noreturn]] void Fail(const char *Fmt, Ts &&...Args) {
-  llvm::WithColor::error(llvm::errs(), ToolName)
-      << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
-  llvm::sys::Process::Exit(1);
+  std::string Message = llvm::formatv(Fmt, std::forward<Ts>(Args)...);
+  Fail(Message.data());
+}
+
+template <typename... Ts> [[noreturn]] void Fail(llvm::Error Err) {
+  std::string Message = toString(std::move(Err));
+  Fail(Message.data());
 }
 
 template <typename... Ts>
@@ -157,13 +161,15 @@ struct SummaryFile {
   static SummaryFile FromPath(llvm::StringRef Path) {
     llvm::StringRef Extension = path::extension(Path);
     if (Extension.empty()) {
-      Fail(ErrorCannotValidateSummary, Path, ErrorExtensionNotSupplied);
+      Fail(ErrorMessages::CannotValidateSummary, Path,
+           ErrorMessages::ExtensionNotSupplied);
     }
     Extension = Extension.drop_front();
     SerializationFormat *Format = GetFormatForExtension(Extension);
     if (!Format) {
-      std::string Suffix = llvm::formatv(ErrorNoFormatForExtension, Extension);
-      Fail(ErrorCannotValidateSummary, Path, Suffix);
+      std::string BadExtension =
+          llvm::formatv(ErrorMessages::NoFormatForExtension, Extension);
+      Fail(ErrorMessages::CannotValidateSummary, Path, BadExtension);
     }
     return {Path.str(), Format};
   }
@@ -175,6 +181,8 @@ struct LinkerInput {
   std::string LinkUnitName;
 };
 
+static void PrintVersion(llvm::raw_ostream &OS) { OS << ToolName << " 0.1\n"; }
+
 //===----------------------------------------------------------------------===//
 // Pipeline
 //===----------------------------------------------------------------------===//
@@ -189,12 +197,13 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
     llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
 
     if (!fs::exists(DirToCheck)) {
-      Fail(ErrorCannotValidateSummary, OutputPath, ErrorOutputDirectoryMissing);
+      Fail(ErrorMessages::CannotValidateSummary, OutputPath,
+           ErrorMessages::OutputDirectoryMissing);
     }
 
     if (fs::access(DirToCheck, fs::AccessMode::Write)) {
-      Fail(ErrorCannotValidateSummary, OutputPath,
-           ErrorOutputDirectoryNotWritable);
+      Fail(ErrorMessages::CannotValidateSummary, OutputPath,
+           ErrorMessages::OutputDirectoryNotWritable);
     }
 
     LI.OutputFile = SummaryFile::FromPath(OutputPath);
@@ -209,7 +218,7 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
       llvm::SmallString<256> RealPath;
       std::error_code EC = fs::real_path(InputPath, RealPath, true);
       if (EC) {
-        Fail(ErrorCannotValidateSummary, InputPath, EC.message());
+        Fail(ErrorMessages::CannotValidateSummary, InputPath, EC.message());
       }
       LI.InputFiles.push_back(SummaryFile::FromPath(RealPath));
     }
@@ -244,8 +253,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
       auto ExpectedSummaryEncoding =
           InputFile.Format->readTUSummaryEncoding(InputFile.Path);
       if (!ExpectedSummaryEncoding) {
-        Fail(ErrorLoadFailed, InputFile.Path,
-             toString(ExpectedSummaryEncoding.takeError()));
+        Fail(ExpectedSummaryEncoding.takeError());
       }
 
       Summary = std::make_unique<TUSummaryEncoding>(
@@ -259,7 +267,9 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
       llvm::TimeRegion _(Time ? &TLink : nullptr);
 
       if (auto Err = EL.link(std::move(Summary))) {
-        Fail(ErrorLinkFailed, InputFile.Path, toString(std::move(Err)));
+        Fail(ErrorBuilder::wrap(std::move(Err))
+                 .context(ErrorMessages::LinkingSummary, InputFile.Path)
+                 .build());
       }
     }
   }
@@ -272,7 +282,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
     auto Output = std::move(EL).getOutput();
     if (auto Err = LI.OutputFile.Format->writeLUSummaryEncoding(
             Output, LI.OutputFile.Path)) {
-      Fail(ErrorWriteFailed, LI.OutputFile.Path, toString(std::move(Err)));
+      Fail(std::move(Err));
     }
   }
 }

>From 15d5ac9de83d4bd485900ef0164b4ccabd902bd9 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 15:44:58 -0800
Subject: [PATCH 05/13] LIT needs to know about the tool

---
 clang/test/lit.cfg.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/clang/test/lit.cfg.py b/clang/test/lit.cfg.py
index 6796c64fc4778..68f460fe5178f 100644
--- a/clang/test/lit.cfg.py
+++ b/clang/test/lit.cfg.py
@@ -123,6 +123,7 @@
         command=FindTool("clang-extdef-mapping"),
         unresolved="ignore",
     ),
+    "ssaf-linker",
 ]
 
 if config.clang_examples:

>From 733e7c83b5070ae1266f2204def2902973facc40 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 15:54:06 -0800
Subject: [PATCH 06/13] Remove unreferenced file

---
 .../ssaf-linker/Inputs/tu-dup-id.json         | 28 -----------------
 .../ssaf-linker/Inputs/tu-dup-namespace.json  |  9 ------
 .../ssaf-linker/Inputs/tu-orphan-data.json    | 30 -------------------
 3 files changed, 67 deletions(-)
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
 delete mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
deleted file mode 100644
index 85e884b96590b..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-id.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
-  "tu_namespace": {
-    "kind": "CompilationUnit",
-    "name": "tu-dup-id.cpp"
-  },
-  "id_table": [
-    {
-      "id": 0,
-      "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu-dup-id.cpp" }],
-        "suffix": "",
-        "usr": "c:@F at foo#"
-      }
-    },
-    {
-      "id": 0,
-      "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu-dup-id.cpp" }],
-        "suffix": "",
-        "usr": "c:@F at bar#"
-      }
-    }
-  ],
-  "linkage_table": [
-    { "id": 0, "linkage": { "type": "External" } }
-  ],
-  "data": []
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
deleted file mode 100644
index 4550e814bceab..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-dup-namespace.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "tu_namespace": {
-    "kind": "CompilationUnit",
-    "name": "tu1.cpp"
-  },
-  "id_table": [],
-  "linkage_table": [],
-  "data": []
-}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json
deleted file mode 100644
index f0aa5507ec19a..0000000000000
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-orphan-data.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
-  "tu_namespace": {
-    "kind": "CompilationUnit",
-    "name": "tu-orphan-data.cpp"
-  },
-  "id_table": [
-    {
-      "id": 0,
-      "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu-orphan-data.cpp" }],
-        "suffix": "",
-        "usr": "c:@F at foo#"
-      }
-    }
-  ],
-  "linkage_table": [
-    { "id": 0, "linkage": { "type": "External" } }
-  ],
-  "data": [
-    {
-      "summary_name": "TestAnalysis",
-      "summary_data": [
-        {
-          "entity_id": 99,
-          "entity_summary": {}
-        }
-      ]
-    }
-  ]
-}

>From 34c4f125ea10ec1cca0daf3b23ca8c61fffb1cc4 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 16:16:11 -0800
Subject: [PATCH 07/13] More fix

---
 clang/test/Analysis/Scalable/ssaf-linker/cli.test | 5 +++--
 clang/tools/ssaf-linker/SSAFLinker.cpp            | 6 +++++-
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/cli.test b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
index 337843562e74b..2ba0edae63fe0 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/cli.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
@@ -2,12 +2,13 @@
 
 // RUN: not ssaf-linker 2>&1 | FileCheck %s --check-prefix=NO-ARGS
 // NO-ARGS: ssaf-linker: Not enough positional command line arguments specified!
-// NO-ARGS: Must specify at least 1 positional argument: See: ssaf-linker --help
+// NO-ARGS: Must specify at least 1 positional argument: See: {{.*}}/ssaf-linker --help
+// NO-ARGS: ssaf-linker: for the -o option: must be specified at least once!
 
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 | FileCheck %s --check-prefix=NO-OUTPUT
 // NO-OUTPUT: ssaf-linker: for the -o option: must be specified at least once!
 
 // RUN: not ssaf-linker -o %t/output.json 2>&1 | FileCheck %s --check-prefix=NO-INPUT
 // NO-INPUT: ssaf-linker: Not enough positional command line arguments specified!
-// NO-INPUT: Must specify at least 1 positional argument: See: ssaf-linker --help
+// NO-INPUT: Must specify at least 1 positional argument: See: {{.*}}/ssaf-linker --help
 
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
index 2be482859a3b1..318a9a5d99795 100644
--- a/clang/tools/ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -296,12 +296,16 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
 int main(int argc, const char **argv) {
   InitLLVM X(argc, argv);
   ToolName = llvm::sys::path::filename(argv[0]);
-  initializeJSONFormat();
 
+  // Hide options unrelated to ssaf-linker from --help output.
   cl::HideUnrelatedOptions(SsafLinkerCategory);
+  // Register a custom version printer for the --version flag.
   cl::SetVersionPrinter(PrintVersion);
+  // Parse command-line arguments and exit with an error if they are invalid.
   cl::ParseCommandLineOptions(argc, argv, "SSAF Linker\n");
 
+  initializeJSONFormat();
+
   llvm::TimerGroup LinkerTimers("ssaf-linker", "SSAF Linker");
   LinkerInput LI;
 

>From 8174c5135ae7321ededb98dbb0f41082e5977f0d Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 16:16:24 -0800
Subject: [PATCH 08/13] Format

---
 .../Scalable/ssaf-linker/Inputs/tu-1.json     | 257 ++++++++++++++---
 .../Scalable/ssaf-linker/Inputs/tu-2.json     | 264 +++++++++++++++---
 .../Inputs/tu-invalid-entity-id-multikey.json |  19 +-
 .../Inputs/tu-invalid-entity-id-ref.json      |  18 +-
 .../Inputs/tu-invalid-entity-id-value.json    |  18 +-
 5 files changed, 496 insertions(+), 80 deletions(-)

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
index b67f9aad690c3..0936ae654069c 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
@@ -7,7 +7,12 @@
     {
       "id": 0,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_ext#"
       }
@@ -15,7 +20,12 @@
     {
       "id": 1,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_int#"
       }
@@ -23,7 +33,12 @@
     {
       "id": 2,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_none#"
       }
@@ -31,7 +46,12 @@
     {
       "id": 3,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_ext_tu1#"
       }
@@ -39,7 +59,12 @@
     {
       "id": 4,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_int_tu1#"
       }
@@ -47,19 +72,54 @@
     {
       "id": 5,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu1.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu1.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_none_tu1#"
       }
     }
   ],
   "linkage_table": [
-    { "id": 0, "linkage": { "type": "External" } },
-    { "id": 1, "linkage": { "type": "Internal" } },
-    { "id": 2, "linkage": { "type": "None" } },
-    { "id": 3, "linkage": { "type": "External" } },
-    { "id": 4, "linkage": { "type": "Internal" } },
-    { "id": 5, "linkage": { "type": "None" } }
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "type": "None"
+      }
+    }
   ],
   "data": [
     {
@@ -67,27 +127,87 @@
       "summary_data": [
         {
           "entity_id": 0,
-          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 3 }] }
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
         },
         {
           "entity_id": 1,
-          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 4 }] }
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
         },
         {
           "entity_id": 2,
-          "entity_summary": { "call_count": 1, "callees": [{ "@": 5 }] }
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 5
+              }
+            ]
+          }
         },
         {
           "entity_id": 3,
-          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 1 }] }
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 1
+              }
+            ]
+          }
         },
         {
           "entity_id": 4,
-          "entity_summary": { "call_count": 1, "callees": [{ "@": 3 }] }
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 3
+              }
+            ]
+          }
         },
         {
           "entity_id": 5,
-          "entity_summary": { "call_count": 3, "callees": [{ "@": 0 }, { "@": 3 }, { "@": 4 }] }
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 3
+              },
+              {
+                "@": 4
+              }
+            ]
+          }
         }
       ]
     },
@@ -97,59 +217,126 @@
         {
           "entity_id": 0,
           "entity_summary": {
-            "direct": { "@": 3 },
+            "direct": {
+              "@": 3
+            },
             "indirect": [
-              { "entity": { "@": 1 }, "level": 1 },
-              { "entity": { "@": 4 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 1,
           "entity_summary": {
-            "direct": { "@": 0 },
+            "direct": {
+              "@": 0
+            },
             "indirect": [
-              { "entity": { "@": 2 }, "level": 1 },
-              { "entity": { "@": 5 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 2,
           "entity_summary": {
-            "direct": { "@": 1 },
+            "direct": {
+              "@": 1
+            },
             "indirect": [
-              { "entity": { "@": 3 }, "level": 1 }
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 1
+              }
             ]
           }
         },
         {
           "entity_id": 3,
           "entity_summary": {
-            "direct": { "@": 4 },
+            "direct": {
+              "@": 4
+            },
             "indirect": [
-              { "entity": { "@": 0 }, "level": 1 },
-              { "entity": { "@": 2 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 4,
           "entity_summary": {
-            "direct": { "@": 5 },
+            "direct": {
+              "@": 5
+            },
             "indirect": [
-              { "entity": { "@": 1 }, "level": 1 }
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              }
             ]
           }
         },
         {
           "entity_id": 5,
           "entity_summary": {
-            "direct": { "@": 2 },
+            "direct": {
+              "@": 2
+            },
             "indirect": [
-              { "entity": { "@": 0 }, "level": 1 },
-              { "entity": { "@": 3 }, "level": 2 },
-              { "entity": { "@": 4 }, "level": 3 }
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 3
+              }
             ]
           }
         }
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
index ac6e385e6b49d..2dedd002c9536 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
@@ -7,7 +7,12 @@
     {
       "id": 0,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_ext#"
       }
@@ -15,7 +20,12 @@
     {
       "id": 1,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_int#"
       }
@@ -23,7 +33,12 @@
     {
       "id": 2,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at shared_none#"
       }
@@ -31,7 +46,12 @@
     {
       "id": 3,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_ext_tu2#"
       }
@@ -39,7 +59,12 @@
     {
       "id": 4,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_int_tu2#"
       }
@@ -47,19 +72,54 @@
     {
       "id": 5,
       "name": {
-        "namespace": [{ "kind": "CompilationUnit", "name": "tu2.cpp" }],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "tu2.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at unique_none_tu2#"
       }
     }
   ],
   "linkage_table": [
-    { "id": 0, "linkage": { "type": "External" } },
-    { "id": 1, "linkage": { "type": "Internal" } },
-    { "id": 2, "linkage": { "type": "None" } },
-    { "id": 3, "linkage": { "type": "External" } },
-    { "id": 4, "linkage": { "type": "Internal" } },
-    { "id": 5, "linkage": { "type": "None" } }
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 1,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 2,
+      "linkage": {
+        "type": "None"
+      }
+    },
+    {
+      "id": 3,
+      "linkage": {
+        "type": "External"
+      }
+    },
+    {
+      "id": 4,
+      "linkage": {
+        "type": "Internal"
+      }
+    },
+    {
+      "id": 5,
+      "linkage": {
+        "type": "None"
+      }
+    }
   ],
   "data": [
     {
@@ -67,27 +127,87 @@
       "summary_data": [
         {
           "entity_id": 0,
-          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 5 }] }
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 5
+              }
+            ]
+          }
         },
         {
           "entity_id": 1,
-          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 3 }] }
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
         },
         {
           "entity_id": 2,
-          "entity_summary": { "call_count": 1, "callees": [{ "@": 4 }] }
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 4
+              }
+            ]
+          }
         },
         {
           "entity_id": 3,
-          "entity_summary": { "call_count": 2, "callees": [{ "@": 0 }, { "@": 2 }] }
+          "entity_summary": {
+            "call_count": 2,
+            "callees": [
+              {
+                "@": 0
+              },
+              {
+                "@": 2
+              }
+            ]
+          }
         },
         {
           "entity_id": 4,
-          "entity_summary": { "call_count": 1, "callees": [{ "@": 3 }] }
+          "entity_summary": {
+            "call_count": 1,
+            "callees": [
+              {
+                "@": 3
+              }
+            ]
+          }
         },
         {
           "entity_id": 5,
-          "entity_summary": { "call_count": 3, "callees": [{ "@": 1 }, { "@": 2 }, { "@": 3 }] }
+          "entity_summary": {
+            "call_count": 3,
+            "callees": [
+              {
+                "@": 1
+              },
+              {
+                "@": 2
+              },
+              {
+                "@": 3
+              }
+            ]
+          }
         }
       ]
     },
@@ -97,60 +217,132 @@
         {
           "entity_id": 0,
           "entity_summary": {
-            "direct": { "@": 3 },
+            "direct": {
+              "@": 3
+            },
             "indirect": [
-              { "entity": { "@": 2 }, "level": 1 },
-              { "entity": { "@": 5 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 1,
           "entity_summary": {
-            "direct": { "@": 0 },
+            "direct": {
+              "@": 0
+            },
             "indirect": [
-              { "entity": { "@": 4 }, "level": 1 }
+              {
+                "entity": {
+                  "@": 4
+                },
+                "level": 1
+              }
             ]
           }
         },
         {
           "entity_id": 2,
           "entity_summary": {
-            "direct": { "@": 5 },
+            "direct": {
+              "@": 5
+            },
             "indirect": [
-              { "entity": { "@": 0 }, "level": 1 },
-              { "entity": { "@": 3 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 3,
           "entity_summary": {
-            "direct": { "@": 1 },
+            "direct": {
+              "@": 1
+            },
             "indirect": [
-              { "entity": { "@": 2 }, "level": 1 },
-              { "entity": { "@": 0 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 4,
           "entity_summary": {
-            "direct": { "@": 2 },
+            "direct": {
+              "@": 2
+            },
             "indirect": [
-              { "entity": { "@": 5 }, "level": 1 },
-              { "entity": { "@": 3 }, "level": 2 }
+              {
+                "entity": {
+                  "@": 5
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 3
+                },
+                "level": 2
+              }
             ]
           }
         },
         {
           "entity_id": 5,
           "entity_summary": {
-            "direct": { "@": 4 },
+            "direct": {
+              "@": 4
+            },
             "indirect": [
-              { "entity": { "@": 1 }, "level": 1 },
-              { "entity": { "@": 2 }, "level": 2 },
-              { "entity": { "@": 0 }, "level": 3 }
+              {
+                "entity": {
+                  "@": 1
+                },
+                "level": 1
+              },
+              {
+                "entity": {
+                  "@": 2
+                },
+                "level": 2
+              },
+              {
+                "entity": {
+                  "@": 0
+                },
+                "level": 3
+              }
             ]
           }
         }
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
index ef05419fb2b0b..a975213c4e61e 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
@@ -7,14 +7,24 @@
     {
       "id": 0,
       "name": {
-        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-multikey.cpp"}],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "invalid-entity-id-multikey.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at foo#"
       }
     }
   ],
   "linkage_table": [
-    {"id": 0, "linkage": {"type": "External"}}
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    }
   ],
   "data": [
     {
@@ -23,7 +33,10 @@
         {
           "entity_id": 0,
           "entity_summary": {
-            "ref": {"@": 0, "extra": "field"}
+            "ref": {
+              "@": 0,
+              "extra": "field"
+            }
           }
         }
       ]
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
index 02b8315bd1679..089a38dc4e7d9 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
@@ -7,14 +7,24 @@
     {
       "id": 0,
       "name": {
-        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-ref.cpp"}],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "invalid-entity-id-ref.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at foo#"
       }
     }
   ],
   "linkage_table": [
-    {"id": 0, "linkage": {"type": "External"}}
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    }
   ],
   "data": [
     {
@@ -23,7 +33,9 @@
         {
           "entity_id": 0,
           "entity_summary": {
-            "ref": {"@": 99}
+            "ref": {
+              "@": 99
+            }
           }
         }
       ]
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
index bf89df75671b3..eedf0aa35b953 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
@@ -7,14 +7,24 @@
     {
       "id": 0,
       "name": {
-        "namespace": [{"kind": "CompilationUnit", "name": "invalid-entity-id-value.cpp"}],
+        "namespace": [
+          {
+            "kind": "CompilationUnit",
+            "name": "invalid-entity-id-value.cpp"
+          }
+        ],
         "suffix": "",
         "usr": "c:@F at foo#"
       }
     }
   ],
   "linkage_table": [
-    {"id": 0, "linkage": {"type": "External"}}
+    {
+      "id": 0,
+      "linkage": {
+        "type": "External"
+      }
+    }
   ],
   "data": [
     {
@@ -23,7 +33,9 @@
         {
           "entity_id": 0,
           "entity_summary": {
-            "ref": {"@": "not-a-number"}
+            "ref": {
+              "@": "not-a-number"
+            }
           }
         }
       ]

>From f14d2dfe782b0bbbc62ff9a6deae103c9c0dfe2a Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Thu, 5 Mar 2026 18:09:11 -0800
Subject: [PATCH 09/13] More

---
 clang/test/CMakeLists.txt | 1 +
 1 file changed, 1 insertion(+)

diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index bee19fd375d3e..24136256883f3 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -106,6 +106,7 @@ list(APPEND CLANG_TEST_DEPS
   clang-sycl-linker
   diagtool
   hmaptool
+  ssaf-linker
   )
 
 if(CLANG_ENABLE_CIR)

>From f93bc8a4e2bf65199ca848c9a5792b3ab0ca3935 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Fri, 6 Mar 2026 13:46:53 -0800
Subject: [PATCH 10/13] Balazs

---
 .../Scalable/EntityLinker/EntityLinker.cpp    |  6 +-
 .../JSONFormat/JSONEntitySummaryEncoding.cpp  | 40 ++++-----
 .../JSONFormat/JSONEntitySummaryEncoding.h    |  8 +-
 .../JSONFormat/JSONFormatImpl.cpp             |  8 +-
 .../Scalable/ssaf-linker/Inputs/tu-1.json     |  4 +-
 .../Scalable/ssaf-linker/Inputs/tu-2.json     |  4 +-
 .../Inputs/tu-invalid-entity-id-multikey.json |  2 +-
 .../Inputs/tu-invalid-entity-id-ref.json      |  2 +-
 .../Inputs/tu-invalid-entity-id-value.json    |  2 +-
 .../Scalable/ssaf-linker/Outputs/lu-1+2.json  |  4 +-
 .../Scalable/ssaf-linker/Outputs/lu-1.json    |  4 +-
 .../Scalable/ssaf-linker/Outputs/lu-2.json    |  4 +-
 .../Analysis/Scalable/ssaf-linker/cli.test    | 19 ++--
 .../Analysis/Scalable/ssaf-linker/help.test   | 31 +++----
 .../Analysis/Scalable/ssaf-linker/io.test     | 19 ++--
 .../Scalable/ssaf-linker/linking-errors.test  | 24 +++---
 .../Analysis/Scalable/ssaf-linker/time.test   | 22 ++---
 .../ssaf-linker/validation-errors.test        | 33 ++++---
 .../Scalable/ssaf-linker/verbose.test         | 28 +++---
 .../Scalable/ssaf-linker/version.test         |  2 +-
 clang/tools/ssaf-linker/SSAFLinker.cpp        | 86 +++++++++----------
 21 files changed, 171 insertions(+), 181 deletions(-)

diff --git a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
index 2cb23718d0fe8..25cad1105e303 100644
--- a/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
+++ b/clang/lib/Analysis/Scalable/EntityLinker/EntityLinker.cpp
@@ -187,9 +187,5 @@ llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
 
   auto EntityResolutionTable = resolve(SummaryRef);
   auto PatchTargets = merge(SummaryRef, EntityResolutionTable);
-  if (auto Err = patch(PatchTargets, EntityResolutionTable)) {
-    return Err;
-  }
-
-  return llvm::Error::success();
+  return patch(PatchTargets, EntityResolutionTable);
 }
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
index 6cab6885db051..35c45ac108e07 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.cpp
@@ -11,13 +11,9 @@
 
 namespace clang::ssaf {
 
-llvm::Expected<bool> JSONEntitySummaryEncoding::patchEntityIdObject(
-    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
-
-  llvm::json::Value *AtVal = Obj.get(JSONEntityIdKey);
-  if (!AtVal) {
-    return false;
-  }
+llvm::Error JSONEntitySummaryEncoding::patchEntityIdObject(
+    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table,
+    llvm::json::Value *AtVal) {
 
   if (Obj.size() != 1) {
     return ErrorBuilder::create(std::errc::invalid_argument,
@@ -45,31 +41,27 @@ llvm::Expected<bool> JSONEntitySummaryEncoding::patchEntityIdObject(
 
   *AtVal = static_cast<uint64_t>(JSONFormat::getIndex(It->second));
 
-  return true;
+  return llvm::Error::success();
 }
 
-llvm::Error JSONEntitySummaryEncoding::patchObject(
+llvm::Error JSONEntitySummaryEncoding::patchRegularObject(
     llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
-
-  auto ExpectedIsEntityId = patchEntityIdObject(Obj, Table);
-
-  if (!ExpectedIsEntityId) {
-    return ExpectedIsEntityId.takeError();
-  }
-
-  bool IsEntityId = *ExpectedIsEntityId;
-
-  if (!IsEntityId) {
-    for (auto &[Key, Val] : Obj) {
-      if (auto Err = patchValue(Val, Table)) {
-        return Err;
-      }
+  for (auto &[Key, Val] : Obj) {
+    if (auto Err = patchValue(Val, Table)) {
+      return Err;
     }
   }
-
   return llvm::Error::success();
 }
 
+llvm::Error JSONEntitySummaryEncoding::patchObject(
+    llvm::json::Object &Obj, const std::map<EntityId, EntityId> &Table) {
+
+  llvm::json::Value *AtVal = Obj.get(JSONEntityIdKey);
+  return AtVal ? patchEntityIdObject(Obj, Table, AtVal)
+               : patchRegularObject(Obj, Table);
+}
+
 llvm::Error JSONEntitySummaryEncoding::patchValue(
     llvm::json::Value &V, const std::map<EntityId, EntityId> &Table) {
   if (llvm::json::Object *Obj = V.getAsObject()) {
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
index aa826671e2706..8475fd9c02851 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONEntitySummaryEncoding.h
@@ -34,9 +34,11 @@ class JSONEntitySummaryEncoding final : public EntitySummaryEncoding {
   explicit JSONEntitySummaryEncoding(llvm::json::Value Data)
       : Data(std::move(Data)) {}
 
-  llvm::Expected<bool>
-  patchEntityIdObject(llvm::json::Object &Obj,
-                      const std::map<EntityId, EntityId> &Table);
+  llvm::Error patchEntityIdObject(llvm::json::Object &Obj,
+                                  const std::map<EntityId, EntityId> &Table,
+                                  llvm::json::Value *AtVal);
+  llvm::Error patchRegularObject(llvm::json::Object &Obj,
+                                 const std::map<EntityId, EntityId> &Table);
   llvm::Error patchObject(llvm::json::Object &Obj,
                           const std::map<EntityId, EntityId> &Table);
   llvm::Error patchValue(llvm::json::Value &V,
diff --git a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
index dc5831d89a147..fa7cd8b7a346a 100644
--- a/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
+++ b/clang/lib/Analysis/Scalable/Serialization/JSONFormat/JSONFormatImpl.cpp
@@ -651,9 +651,8 @@ JSONFormat::entitySummaryFromJSON(const SummaryName &SN,
   const auto &InfoEntry = InfoIt->second;
   assert(InfoEntry.ForSummary == SN);
 
-  return InfoEntry.Deserialize(
-      EntitySummaryObject, IdTable,
-      [](const Object &Obj) { return entityIdFromJSONObject(Obj); });
+  return InfoEntry.Deserialize(EntitySummaryObject, IdTable,
+                               entityIdFromJSONObject);
 }
 
 llvm::Expected<Object>
@@ -670,8 +669,7 @@ JSONFormat::entitySummaryToJSON(const SummaryName &SN,
   const auto &InfoEntry = InfoIt->second;
   assert(InfoEntry.ForSummary == SN);
 
-  return InfoEntry.Serialize(
-      ES, [](EntityId EI) { return entityIdToJSONObject(EI); });
+  return InfoEntry.Serialize(ES, entityIdToJSONObject);
 }
 
 //----------------------------------------------------------------------------
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
index 0936ae654069c..4606f2532df5a 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-1.json
@@ -123,7 +123,7 @@
   ],
   "data": [
     {
-      "summary_name": "CallGraph",
+      "summary_name": "Analysis1",
       "summary_data": [
         {
           "entity_id": 0,
@@ -212,7 +212,7 @@
       ]
     },
     {
-      "summary_name": "TypeInfo",
+      "summary_name": "Analysis2",
       "summary_data": [
         {
           "entity_id": 0,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
index 2dedd002c9536..ce4bf3e0307aa 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-2.json
@@ -123,7 +123,7 @@
   ],
   "data": [
     {
-      "summary_name": "CallGraph",
+      "summary_name": "Analysis1",
       "summary_data": [
         {
           "entity_id": 0,
@@ -212,7 +212,7 @@
       ]
     },
     {
-      "summary_name": "TypeInfo",
+      "summary_name": "Analysis2",
       "summary_data": [
         {
           "entity_id": 0,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
index a975213c4e61e..30798eaa1786e 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-multikey.json
@@ -28,7 +28,7 @@
   ],
   "data": [
     {
-      "summary_name": "TestAnalysis",
+      "summary_name": "Analysis1",
       "summary_data": [
         {
           "entity_id": 0,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
index 089a38dc4e7d9..2db2c6ea9b3c1 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-ref.json
@@ -28,7 +28,7 @@
   ],
   "data": [
     {
-      "summary_name": "TestAnalysis",
+      "summary_name": "Analysis1",
       "summary_data": [
         {
           "entity_id": 0,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
index eedf0aa35b953..7a66281f7ac41 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-invalid-entity-id-value.json
@@ -28,7 +28,7 @@
   ],
   "data": [
     {
-      "summary_name": "TestAnalysis",
+      "summary_name": "Analysis1",
       "summary_data": [
         {
           "entity_id": 0,
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
index 6a74d1e341c2c..568a175730ce6 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1+2.json
@@ -154,7 +154,7 @@
           }
         }
       ],
-      "summary_name": "CallGraph"
+      "summary_name": "Analysis1"
     },
     {
       "summary_data": [
@@ -395,7 +395,7 @@
           }
         }
       ],
-      "summary_name": "TypeInfo"
+      "summary_name": "Analysis2"
     }
   ],
   "id_table": [
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
index 3e5e17ce3f0c0..004e5490bba26 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-1.json
@@ -87,7 +87,7 @@
           }
         }
       ],
-      "summary_name": "CallGraph"
+      "summary_name": "Analysis1"
     },
     {
       "summary_data": [
@@ -218,7 +218,7 @@
           }
         }
       ],
-      "summary_name": "TypeInfo"
+      "summary_name": "Analysis2"
     }
   ],
   "id_table": [
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
index a60887a53cb83..1ed0e7fd38f6c 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/lu-2.json
@@ -87,7 +87,7 @@
           }
         }
       ],
-      "summary_name": "CallGraph"
+      "summary_name": "Analysis1"
     },
     {
       "summary_data": [
@@ -224,7 +224,7 @@
           }
         }
       ],
-      "summary_name": "TypeInfo"
+      "summary_name": "Analysis2"
     }
   ],
   "id_table": [
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/cli.test b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
index 2ba0edae63fe0..79775c3f9c6ba 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/cli.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
@@ -1,14 +1,17 @@
 // Tests for ssaf-linker command-line option validation.
 
-// RUN: not ssaf-linker 2>&1 | FileCheck %s --check-prefix=NO-ARGS
-// NO-ARGS: ssaf-linker: Not enough positional command line arguments specified!
-// NO-ARGS: Must specify at least 1 positional argument: See: {{.*}}/ssaf-linker --help
-// NO-ARGS: ssaf-linker: for the -o option: must be specified at least once!
+// RUN: not ssaf-linker 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-ARGS
+// NO-ARGS:      ssaf-linker: Not enough positional command line arguments specified!
+// NO-ARGS-NEXT: Must specify at least 1 positional argument: See: {{.*}}ssaf-linker{{(\.exe)?}} --help
+// NO-ARGS-NEXT: ssaf-linker: for the -o option: must be specified at least once!
 
-// RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 | FileCheck %s --check-prefix=NO-OUTPUT
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT
 // NO-OUTPUT: ssaf-linker: for the -o option: must be specified at least once!
 
-// RUN: not ssaf-linker -o %t/output.json 2>&1 | FileCheck %s --check-prefix=NO-INPUT
-// NO-INPUT: ssaf-linker: Not enough positional command line arguments specified!
-// NO-INPUT: Must specify at least 1 positional argument: See: {{.*}}/ssaf-linker --help
+// RUN: not ssaf-linker -o %t/output.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT
+// NO-INPUT:      ssaf-linker: Not enough positional command line arguments specified!
+// NO-INPUT-NEXT: Must specify at least 1 positional argument: See: {{.*}}ssaf-linker{{(\.exe)?}} --help
 
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index d239fba48092a..71445a8bd91bb 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -1,20 +1,21 @@
 // Test ssaf-linker help option
 
-// RUN: ssaf-linker --help-list-hidden | FileCheck %s
+// RUN: ssaf-linker --help-list-hidden \
+// RUN:           | FileCheck %s --match-full-lines
 
-// CHECK: OVERVIEW: SSAF Linker
+// CHECK:      OVERVIEW: SSAF Linker
 // CHECK-EMPTY:
-// CHECK: USAGE: ssaf-linker [options] <input files>
+// CHECK-NEXT: USAGE: ssaf-linker{{(\.exe)?}} [options] <input files>
 // CHECK-EMPTY:
-// CHECK: OPTIONS:
-// CHECK:   -h                  - Alias for --help
-// CHECK:   --help              - Display available options (--help-hidden for more)
-// CHECK:   --help-hidden       - Display all available options
-// CHECK:   --help-list         - Display list of available options (--help-list-hidden for more)
-// CHECK:   --help-list-hidden  - Display list of all available options
-// CHECK:   -o <path>           - Output summary path
-// CHECK:   --print-all-options - Print all option values after command line parsing
-// CHECK:   --print-options     - Print non-default options after command line parsing
-// CHECK:   --time              - Enable timing
-// CHECK:   --verbose           - Enable verbose output
-// CHECK:   --version           - Display the version of this program
\ No newline at end of file
+// CHECK-NEXT: OPTIONS:
+// CHECK-NEXT:   -h                  - Alias for --help
+// CHECK-NEXT:   --help              - Display available options (--help-hidden for more)
+// CHECK-NEXT:   --help-hidden       - Display all available options
+// CHECK-NEXT:   --help-list         - Display list of available options (--help-list-hidden for more)
+// CHECK-NEXT:   --help-list-hidden  - Display list of all available options
+// CHECK-NEXT:   -o <path>           - Output summary path
+// CHECK-NEXT:   --print-all-options - Print all option values after command line parsing
+// CHECK-NEXT:   --print-options     - Print non-default options after command line parsing
+// CHECK-NEXT:   --time              - Enable timing
+// CHECK-NEXT:   --verbose           - Enable verbose output
+// CHECK-NEXT:   --version           - Display the version of this program
\ No newline at end of file
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/io.test b/clang/test/Analysis/Scalable/ssaf-linker/io.test
index 96ff839c362e7..213cf3c23c511 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/io.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/io.test
@@ -5,20 +5,19 @@
 
 // Malformed JSON input.
 // RUN: not ssaf-linker %S/Inputs/tu-malformed.json -o %t/out.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-JSON -DPATH=%S/Inputs/tu-malformed.json
-// BAD-JSON: ssaf-linker: error: reading TUSummary from file '[[PATH]]'
-// BAD-JSON: Invalid JSON value
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=BAD-JSON
+// BAD-JSON:      ssaf-linker: error: reading TUSummary from file '{{.*}}tu-malformed.json'
+// BAD-JSON-NEXT: {{.*}}: Invalid JSON value{{.*}}
 
 // Missing required fields in otherwise valid JSON.
 // RUN: not ssaf-linker %S/Inputs/tu-missing-fields.json -o %t/out.json 2>&1 \
-//        | FileCheck %s --check-prefix=MISSING-FIELDS -DPATH=%S/Inputs/tu-missing-fields.json
-// MISSING-FIELDS: ssaf-linker: error: reading TUSummary from file '[[PATH]]'
-// MISSING-FIELDS: failed to read IdTable from field 'id_table': expected JSON array
+// RUN:        | FileCheck %s --match-full-lines --check-prefix=MISSING-FIELDS
+// MISSING-FIELDS:      ssaf-linker: error: reading TUSummary from file '{{.*}}tu-missing-fields.json'
+// MISSING-FIELDS-NEXT: failed to read IdTable from field 'id_table': expected JSON array
 
 // Output file already exists.
 // RUN: touch %t/out.json
-// RUN: ssaf-linker %S/Inputs/tu-empty.json -o %t/out.json
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/out.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=OUTPUT-EXISTS -DPATH=%t/out.json
-// OUTPUT-EXISTS: ssaf-linker: error: writing LUSummary to file '[[PATH]]'
-// OUTPUT-EXISTS: failed to write file '[[PATH]]': file already exists
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=OUTPUT-EXISTS
+// OUTPUT-EXISTS:      ssaf-linker: error: writing LUSummary to file '{{.*}}out.json'
+// OUTPUT-EXISTS-NEXT: failed to write file '{{.*}}out.json': file already exists
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
index 13b9f471df7f8..87f1f08a6b2ef 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
@@ -5,24 +5,24 @@
 
 // Linking the same TU namespace twice produces an error.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json %S/Inputs/tu-empty.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=DUP-NS -DPATH=%S/Inputs/tu-empty.json
-// DUP-NS: ssaf-linker: error: Linking summary '[[PATH]]'
-// DUP-NS: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp)
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=DUP-NS
+// DUP-NS:      ssaf-linker: error: Linking summary '{{.*}}tu-empty.json'
+// DUP-NS-NEXT: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp)
 
 // Entity ID object in summary data blob with '@' key alongside extra keys is a fatal error.
 // RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-multikey.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=INVALID-ID-MULTIKEY -DPATH=%S/Inputs/tu-invalid-entity-id-multikey.json
-// INVALID-ID-MULTIKEY: ssaf-linker: error: Linking summary '[[PATH]]'
-// INVALID-ID-MULTIKEY: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-MULTIKEY
+// INVALID-ID-MULTIKEY:      ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-multikey.json'
+// INVALID-ID-MULTIKEY-NEXT: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
 
 // Entity ID object in summary data blob with a non-uint64 '@' value is a fatal error.
 // RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-value.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=INVALID-ID-VALUE -DPATH=%S/Inputs/tu-invalid-entity-id-value.json
-// INVALID-ID-VALUE: ssaf-linker: error: Linking summary '[[PATH]]'
-// INVALID-ID-VALUE: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-VALUE
+// INVALID-ID-VALUE:      ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-value.json'
+// INVALID-ID-VALUE-NEXT: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
 
 // Entity ID reference in summary data blob pointing to an ID absent from the resolution table
 // RUN: not ssaf-linker %S/Inputs/tu-invalid-entity-id-ref.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=INVALID-ID-REF -DPATH=%S/Inputs/tu-invalid-entity-id-ref.json
-// INVALID-ID-REF: ssaf-linker: error: Linking summary '[[PATH]]'
-// INVALID-ID-REF: failed to patch EntityId: 'EntityId(99)' not found in entity resolution table
\ No newline at end of file
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-REF
+// INVALID-ID-REF:      ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-ref.json'
+// INVALID-ID-REF-NEXT: failed to patch EntityId: 'EntityId(99)' not found in entity resolution table
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/time.test b/clang/test/Analysis/Scalable/ssaf-linker/time.test
index 8737c6d9e0eef..1c59a19fe35f0 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/time.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/time.test
@@ -4,14 +4,14 @@
 // RUN: mkdir -p %t
 
 // RUN: ssaf-linker --time %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s
-// CHECK: ===-------------------------------------------------------------------------===
-// CHECK:                                   SSAF Linker
-// CHECK: ===-------------------------------------------------------------------------===
-// CHECK: Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
-// CHECK: ---User Time---   --System Time--   --User+System--   ---Wall Time---
-// CHECK-DAG: {{.*}}Write Summary
-// CHECK-DAG: {{.*}}Read Summaries
-// CHECK-DAG: {{.*}}Link Summaries
-// CHECK-DAG: {{.*}}Validate Input
-// CHECK: {{.*}}Total
+// RUN:           | FileCheck %s --match-full-lines
+// CHECK:      ===-------------------------------------------------------------------------===
+// CHECK-NEXT: {{[ ]+}}SSAF Linker
+// CHECK-NEXT: ===-------------------------------------------------------------------------===
+// CHECK-NEXT:   Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
+// CHECK:      {{.*}}---Wall Time---{{.*}}
+// CHECK-DAG:  {{.*}}Write Summary
+// CHECK-DAG:  {{.*}}Read Summaries
+// CHECK-DAG:  {{.*}}Link Summaries
+// CHECK-DAG:  {{.*}}Validate Input
+// CHECK:      {{.*}}Total
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index 50709846aebe9..efdd9756ff8af 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -5,46 +5,45 @@
 
 // No extension on output.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-EXT-OUTPUT
-// NO-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu': Extension not supplied.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-EXT-OUTPUT
+// NO-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu': Extension not supplied
 
 // No extension on input.
 // RUN: not ssaf-linker %S/Inputs/tu-noext -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-EXT-INPUT -DPATH=%S/Inputs/tu-noext
-// NO-EXT-INPUT: ssaf-linker: error: failed to validate summary '[[PATH]]': Extension not supplied.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-EXT-INPUT
+// NO-EXT-INPUT: ssaf-linker: error: failed to validate summary '{{.*}}tu-noext': Extension not supplied
 
 // Invalid extension on output.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o lu.txt 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-EXT-OUTPUT
-// BAD-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu.txt': Format not registered for extension 'txt'.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=BAD-EXT-OUTPUT
+// BAD-EXT-OUTPUT: ssaf-linker: error: failed to validate summary 'lu.txt': Format not registered for extension 'txt'
 
 // Invalid extension on input.
 // RUN: not ssaf-linker %S/Inputs/tu-badext.txt -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BAD-EXT-INPUT -DPATH=%S/Inputs/tu-badext.txt
-// BAD-EXT-INPUT: ssaf-linker: error: failed to validate summary '[[PATH]]': Format not registered for extension 'txt'.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=BAD-EXT-INPUT
+// BAD-EXT-INPUT: ssaf-linker: error: failed to validate summary '{{.*}}tu-badext.txt': Format not registered for extension 'txt'
 
 // Output directory does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %S/Outputs/NonExistentDirectory/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=OUTPUT-PARENT-DIR-MISSING -DPATH=%S/Outputs/NonExistentDirectory/lu.json
-// OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: failed to validate summary '[[PATH]]': Parent directory does not exist.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=OUTPUT-PARENT-DIR-MISSING
+// OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: failed to validate summary '{{.*}}lu.json': Parent directory does not exist
 
 // Output parent directory exists but is not writable.
 // UNSUPPORTED: system-windows
 // RUN: mkdir -p %t/output-dir
 // RUN: chmod -w %t/output-dir
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/output-dir/output.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-WRITE-PERM -DPATH=%t/output-dir/output.json
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-WRITE-PERM
 // RUN: chmod +w %t/output-dir
-// NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '[[PATH]]': Parent directory is not writable.
+// NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '{{.*}}output.json': Parent directory is not writable
 
 // Input summary does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-nonexistent.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=NO-INPUT-FILE -DPATH=%S/Inputs/tu-nonexistent.json
-// NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '[[PATH]]': No such file or directory.
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT-FILE
+// NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '{{.*}}tu-nonexistent.json': No such file or directory
 
 // Input summary is a broken symlink.
 // RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
 // RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --check-prefix=BROKEN-SYMLINK -DPATH=%t/tu-dangling.json
-// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '[[PATH]]': No such file or directory.
-
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=BROKEN-SYMLINK
+// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '{{.*}}tu-dangling.json': No such file or directory
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
index 0f7777043d1d9..8e246efd71f2a 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
@@ -4,17 +4,17 @@
 // RUN: mkdir -p %t
 
 // RUN: ssaf-linker --verbose %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json 2>&1 \
-// RUN:           | FileCheck %s -DINPUT=%S/Inputs -DOUTPUT=%t
-// CHECK: note: - Linking started.
-// CHECK: note:   - Validating input.
-// CHECK: note:     - Validated output summary path '[[OUTPUT]]/lu-1+2.json'.
-// CHECK: note:     - Validated 2 input summary paths.
-// CHECK: note:   - Linking input.
-// CHECK: note:     - Constructing linker.
-// CHECK: note:     - Linking summaries.
-// CHECK: note:       - [1/2] Reading '[[INPUT]]/tu-1.json'.
-// CHECK: note:       - [1/2] Linking '[[INPUT]]/tu-1.json'.
-// CHECK: note:       - [2/2] Reading '[[INPUT]]/tu-2.json'.
-// CHECK: note:       - [2/2] Linking '[[INPUT]]/tu-2.json'.
-// CHECK: note:     - Writing output summary to '[[OUTPUT]]/lu-1+2.json'.
-// CHECK: note: - Linking finished.
\ No newline at end of file
+// RUN:           | FileCheck %s --match-full-lines
+// CHECK:      note: - Linking started.
+// CHECK-NEXT: note:   - Validating input.
+// CHECK-NEXT: note:     - Validated output summary path '{{.*}}lu-1+2.json'.
+// CHECK-NEXT: note:     - Validated 2 input summary paths.
+// CHECK-NEXT: note:   - Linking input.
+// CHECK-NEXT: note:     - Constructing linker.
+// CHECK-NEXT: note:     - Linking summaries.
+// CHECK-NEXT: note:       - [1/2] Reading '{{.*}}tu-1.json'.
+// CHECK-NEXT: note:       - [1/2] Linking '{{.*}}tu-1.json'.
+// CHECK-NEXT: note:       - [2/2] Reading '{{.*}}tu-2.json'.
+// CHECK-NEXT: note:       - [2/2] Linking '{{.*}}tu-2.json'.
+// CHECK-NEXT: note:     - Writing output summary to '{{.*}}lu-1+2.json'.
+// CHECK-NEXT: note: - Linking finished.
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/version.test b/clang/test/Analysis/Scalable/ssaf-linker/version.test
index e68e4eeb47152..ed0b4c2ab56bc 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/version.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/version.test
@@ -1,5 +1,5 @@
 // Test ssaf-linker version
 
-// RUN: ssaf-linker --version | FileCheck %s
+// RUN: ssaf-linker --version | FileCheck %s --match-full-lines
 
 // CHECK: ssaf-linker 0.1
diff --git a/clang/tools/ssaf-linker/SSAFLinker.cpp b/clang/tools/ssaf-linker/SSAFLinker.cpp
index 318a9a5d99795..66cf8b368487c 100644
--- a/clang/tools/ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/ssaf-linker/SSAFLinker.cpp
@@ -66,18 +66,18 @@ cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
 namespace ErrorMessages {
 
 constexpr const char *CannotValidateSummary =
-    "failed to validate summary '{0}': {1}.";
+    "failed to validate summary '{0}': {1}";
 
 constexpr const char *OutputDirectoryMissing =
-    "Parent directory does not exist.";
+    "Parent directory does not exist";
 
 constexpr const char *OutputDirectoryNotWritable =
-    "Parent directory is not writable.";
+    "Parent directory is not writable";
 
-constexpr const char *ExtensionNotSupplied = "Extension not supplied.";
+constexpr const char *ExtensionNotSupplied = "Extension not supplied";
 
 constexpr const char *NoFormatForExtension =
-    "Format not registered for extension '{0}'.";
+    "Format not registered for extension '{0}'";
 
 constexpr const char *LinkingSummary = "Linking summary '{0}'";
 
@@ -91,24 +91,23 @@ constexpr unsigned IndentationWidth = 2;
 
 llvm::StringRef ToolName;
 
-template <typename... Ts> [[noreturn]] void Fail(const char *Msg) {
+template <typename... Ts> [[noreturn]] void fail(const char *Msg) {
   llvm::WithColor::error(llvm::errs(), ToolName) << Msg << "\n";
   llvm::sys::Process::Exit(1);
 }
 
 template <typename... Ts>
-[[noreturn]] void Fail(const char *Fmt, Ts &&...Args) {
+[[noreturn]] void fail(const char *Fmt, Ts &&...Args) {
   std::string Message = llvm::formatv(Fmt, std::forward<Ts>(Args)...);
-  Fail(Message.data());
+  fail(Message.data());
 }
 
-template <typename... Ts> [[noreturn]] void Fail(llvm::Error Err) {
-  std::string Message = toString(std::move(Err));
-  Fail(Message.data());
+template <typename... Ts> [[noreturn]] void fail(llvm::Error Err) {
+  fail(toString(std::move(Err)).data());
 }
 
 template <typename... Ts>
-void Info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
+void info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
   if (Verbose) {
     llvm::WithColor::note()
         << std::string(IndentationLevel * IndentationWidth, ' ') << "- "
@@ -120,7 +119,7 @@ void Info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
 // Format Registry
 //===----------------------------------------------------------------------===//
 
-SerializationFormat *GetFormatForExtension(llvm::StringRef Extension) {
+SerializationFormat *getFormatForExtension(llvm::StringRef Extension) {
   static llvm::SmallVector<
       std::pair<std::string, std::unique_ptr<SerializationFormat>>, 4>
       ExtensionFormatList;
@@ -158,18 +157,18 @@ struct SummaryFile {
   std::string Path;
   SerializationFormat *Format = nullptr;
 
-  static SummaryFile FromPath(llvm::StringRef Path) {
+  static SummaryFile fromPath(llvm::StringRef Path) {
     llvm::StringRef Extension = path::extension(Path);
     if (Extension.empty()) {
-      Fail(ErrorMessages::CannotValidateSummary, Path,
+      fail(ErrorMessages::CannotValidateSummary, Path,
            ErrorMessages::ExtensionNotSupplied);
     }
     Extension = Extension.drop_front();
-    SerializationFormat *Format = GetFormatForExtension(Extension);
+    SerializationFormat *Format = getFormatForExtension(Extension);
     if (!Format) {
       std::string BadExtension =
           llvm::formatv(ErrorMessages::NoFormatForExtension, Extension);
-      Fail(ErrorMessages::CannotValidateSummary, Path, BadExtension);
+      fail(ErrorMessages::CannotValidateSummary, Path, BadExtension);
     }
     return {Path.str(), Format};
   }
@@ -181,13 +180,13 @@ struct LinkerInput {
   std::string LinkUnitName;
 };
 
-static void PrintVersion(llvm::raw_ostream &OS) { OS << ToolName << " 0.1\n"; }
+static void printVersion(llvm::raw_ostream &OS) { OS << ToolName << " 0.1\n"; }
 
 //===----------------------------------------------------------------------===//
 // Pipeline
 //===----------------------------------------------------------------------===//
 
-LinkerInput Validate(llvm::TimerGroup &TG) {
+LinkerInput validate(llvm::TimerGroup &TG) {
   llvm::Timer TValidate("validate", "Validate Input", TG);
   LinkerInput LI;
 
@@ -197,20 +196,20 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
     llvm::StringRef DirToCheck = ParentDir.empty() ? "." : ParentDir;
 
     if (!fs::exists(DirToCheck)) {
-      Fail(ErrorMessages::CannotValidateSummary, OutputPath,
+      fail(ErrorMessages::CannotValidateSummary, OutputPath,
            ErrorMessages::OutputDirectoryMissing);
     }
 
     if (fs::access(DirToCheck, fs::AccessMode::Write)) {
-      Fail(ErrorMessages::CannotValidateSummary, OutputPath,
+      fail(ErrorMessages::CannotValidateSummary, OutputPath,
            ErrorMessages::OutputDirectoryNotWritable);
     }
 
-    LI.OutputFile = SummaryFile::FromPath(OutputPath);
+    LI.OutputFile = SummaryFile::fromPath(OutputPath);
     LI.LinkUnitName = path::stem(LI.OutputFile.Path).str();
   }
 
-  Info(2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
+  info(2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
 
   {
     llvm::TimeRegion _(Time ? &TValidate : nullptr);
@@ -218,19 +217,19 @@ LinkerInput Validate(llvm::TimerGroup &TG) {
       llvm::SmallString<256> RealPath;
       std::error_code EC = fs::real_path(InputPath, RealPath, true);
       if (EC) {
-        Fail(ErrorMessages::CannotValidateSummary, InputPath, EC.message());
+        fail(ErrorMessages::CannotValidateSummary, InputPath, EC.message());
       }
-      LI.InputFiles.push_back(SummaryFile::FromPath(RealPath));
+      LI.InputFiles.push_back(SummaryFile::fromPath(RealPath));
     }
   }
 
-  Info(2, "Validated {0} input summary paths.", LI.InputFiles.size());
+  info(2, "Validated {0} input summary paths.", LI.InputFiles.size());
 
   return LI;
 }
 
-void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
-  Info(2, "Constructing linker.");
+void link(const LinkerInput &LI, llvm::TimerGroup &TG) {
+  info(2, "Constructing linker.");
 
   EntityLinker EL(NestedBuildNamespace(
       BuildNamespace(BuildNamespaceKind::LinkUnit, LI.LinkUnitName)));
@@ -239,13 +238,13 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
   llvm::Timer TLink("link", "Link Summaries", TG);
   llvm::Timer TWrite("write", "Write Summary", TG);
 
-  Info(2, "Linking summaries.");
+  info(2, "Linking summaries.");
 
   for (auto [Index, InputFile] : llvm::enumerate(LI.InputFiles)) {
     std::unique_ptr<TUSummaryEncoding> Summary;
 
     {
-      Info(3, "[{0}/{1}] Reading '{2}'.", (Index + 1), LI.InputFiles.size(),
+      info(3, "[{0}/{1}] Reading '{2}'.", (Index + 1), LI.InputFiles.size(),
            InputFile.Path);
 
       llvm::TimeRegion _(Time ? &TRead : nullptr);
@@ -253,7 +252,7 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
       auto ExpectedSummaryEncoding =
           InputFile.Format->readTUSummaryEncoding(InputFile.Path);
       if (!ExpectedSummaryEncoding) {
-        Fail(ExpectedSummaryEncoding.takeError());
+        fail(ExpectedSummaryEncoding.takeError());
       }
 
       Summary = std::make_unique<TUSummaryEncoding>(
@@ -261,13 +260,13 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
     }
 
     {
-      Info(3, "[{0}/{1}] Linking '{2}'.", (Index + 1), LI.InputFiles.size(),
+      info(3, "[{0}/{1}] Linking '{2}'.", (Index + 1), LI.InputFiles.size(),
            InputFile.Path);
 
       llvm::TimeRegion _(Time ? &TLink : nullptr);
 
       if (auto Err = EL.link(std::move(Summary))) {
-        Fail(ErrorBuilder::wrap(std::move(Err))
+        fail(ErrorBuilder::wrap(std::move(Err))
                  .context(ErrorMessages::LinkingSummary, InputFile.Path)
                  .build());
       }
@@ -275,14 +274,14 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
   }
 
   {
-    Info(2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
+    info(2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
 
     llvm::TimeRegion _(Time ? &TWrite : nullptr);
 
     auto Output = std::move(EL).getOutput();
     if (auto Err = LI.OutputFile.Format->writeLUSummaryEncoding(
             Output, LI.OutputFile.Path)) {
-      Fail(std::move(Err));
+      fail(std::move(Err));
     }
   }
 }
@@ -295,12 +294,13 @@ void Link(const LinkerInput &LI, llvm::TimerGroup &TG) {
 
 int main(int argc, const char **argv) {
   InitLLVM X(argc, argv);
-  ToolName = llvm::sys::path::filename(argv[0]);
+  // path::stem strips the .exe extension on Windows so ToolName is consistent.
+  ToolName = llvm::sys::path::stem(argv[0]);
 
   // Hide options unrelated to ssaf-linker from --help output.
   cl::HideUnrelatedOptions(SsafLinkerCategory);
   // Register a custom version printer for the --version flag.
-  cl::SetVersionPrinter(PrintVersion);
+  cl::SetVersionPrinter(printVersion);
   // Parse command-line arguments and exit with an error if they are invalid.
   cl::ParseCommandLineOptions(argc, argv, "SSAF Linker\n");
 
@@ -310,19 +310,19 @@ int main(int argc, const char **argv) {
   LinkerInput LI;
 
   {
-    Info(0, "Linking started.");
+    info(0, "Linking started.");
 
     {
-      Info(1, "Validating input.");
-      LI = Validate(LinkerTimers);
+      info(1, "Validating input.");
+      LI = validate(LinkerTimers);
     }
 
     {
-      Info(1, "Linking input.");
-      Link(LI, LinkerTimers);
+      info(1, "Linking input.");
+      link(LI, LinkerTimers);
     }
 
-    Info(0, "Linking finished.");
+    info(0, "Linking finished.");
   }
 
   return 0;

>From 2f47726836af6e6339d0ba4099cb8b3f0fe49121 Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Sat, 7 Mar 2026 11:37:06 -0800
Subject: [PATCH 11/13] More fixes

---
 clang/test/Analysis/Scalable/ssaf-linker/cli.test  |  8 ++++----
 .../Analysis/Scalable/ssaf-linker/linking.test     | 14 +++++++++++++-
 .../ssaf-linker/validation-errors-permissions.test | 13 +++++++++++++
 .../Scalable/ssaf-linker/validation-errors.test    |  9 ---------
 .../Analysis/Scalable/ssaf-linker/version.test     |  4 ++--
 5 files changed, 32 insertions(+), 16 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/cli.test b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
index 79775c3f9c6ba..3a4bb7d6e1faf 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/cli.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/cli.test
@@ -2,16 +2,16 @@
 
 // RUN: not ssaf-linker 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-ARGS
-// NO-ARGS:      ssaf-linker: Not enough positional command line arguments specified!
+// NO-ARGS:      ssaf-linker{{(\.exe)?}}: Not enough positional command line arguments specified!
 // NO-ARGS-NEXT: Must specify at least 1 positional argument: See: {{.*}}ssaf-linker{{(\.exe)?}} --help
-// NO-ARGS-NEXT: ssaf-linker: for the -o option: must be specified at least once!
+// NO-ARGS-NEXT: ssaf-linker{{(\.exe)?}}: for the -o option: must be specified at least once!
 
 // RUN: not ssaf-linker %S/Inputs/tu-empty.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-OUTPUT
-// NO-OUTPUT: ssaf-linker: for the -o option: must be specified at least once!
+// NO-OUTPUT: ssaf-linker{{(\.exe)?}}: for the -o option: must be specified at least once!
 
 // RUN: not ssaf-linker -o %t/output.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT
-// NO-INPUT:      ssaf-linker: Not enough positional command line arguments specified!
+// NO-INPUT:      ssaf-linker{{(\.exe)?}}: Not enough positional command line arguments specified!
 // NO-INPUT-NEXT: Must specify at least 1 positional argument: See: {{.*}}ssaf-linker{{(\.exe)?}} --help
 
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking.test b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
index 63e501811334d..2ed6d37b558ac 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
@@ -27,4 +27,16 @@
 // Linking two TUs correctly resolves and patches data.
 // RUN: ssaf-linker %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json
 // RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
-// RUN: rm %t/lu-1+2.json
\ No newline at end of file
+// RUN: rm %t/lu-1+2.json
+
+// Relative input path: cd to the test source directory so that
+// Inputs/tu-1.json is a valid relative path.
+// RUN: cd %S && ssaf-linker Inputs/tu-1.json -o %t/lu-1.json
+// RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
+// RUN: rm %t/lu-1.json
+
+// Relative output path: cd to the temp directory so that lu-1.json
+// (no directory component) resolves relative to the current directory.
+// RUN: cd %t && ssaf-linker %S/Inputs/tu-1.json -o lu-1.json
+// RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
+// RUN: rm %t/lu-1.json
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test
new file mode 100644
index 0000000000000..801b67b7ba777
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test
@@ -0,0 +1,13 @@
+// Tests for ssaf-linker input validation requiring file permission support.
+// UNSUPPORTED: system-windows
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// Output parent directory exists but is not writable.
+// RUN: mkdir -p %t/output-dir
+// RUN: chmod -w %t/output-dir
+// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/output-dir/output.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-WRITE-PERM
+// RUN: chmod +w %t/output-dir
+// NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '{{.*}}output.json': Parent directory is not writable
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index efdd9756ff8af..563edde592b97 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -28,15 +28,6 @@
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=OUTPUT-PARENT-DIR-MISSING
 // OUTPUT-PARENT-DIR-MISSING: ssaf-linker: error: failed to validate summary '{{.*}}lu.json': Parent directory does not exist
 
-// Output parent directory exists but is not writable.
-// UNSUPPORTED: system-windows
-// RUN: mkdir -p %t/output-dir
-// RUN: chmod -w %t/output-dir
-// RUN: not ssaf-linker %S/Inputs/tu-empty.json -o %t/output-dir/output.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-WRITE-PERM
-// RUN: chmod +w %t/output-dir
-// NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '{{.*}}output.json': Parent directory is not writable
-
 // Input summary does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-nonexistent.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT-FILE
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/version.test b/clang/test/Analysis/Scalable/ssaf-linker/version.test
index ed0b4c2ab56bc..e6e9e41663199 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/version.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/version.test
@@ -1,5 +1,5 @@
 // Test ssaf-linker version
 
-// RUN: ssaf-linker --version | FileCheck %s --match-full-lines
+// RUN: ssaf-linker --version | FileCheck %s
 
-// CHECK: ssaf-linker 0.1
+// CHECK: ssaf-linker

>From c499de3aa5120edd0578117dbea27c26b92d43ca Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Sat, 7 Mar 2026 14:32:40 -0800
Subject: [PATCH 12/13] Fix

---
 .../test/Analysis/Scalable/ssaf-linker/validation-errors.test | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index 563edde592b97..5a1c37f87f5e2 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -31,10 +31,10 @@
 // Input summary does not exist.
 // RUN: not ssaf-linker %S/Inputs/tu-nonexistent.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT-FILE
-// NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '{{.*}}tu-nonexistent.json': No such file or directory
+// NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '{{.*}}tu-nonexistent.json': {{[Nn]}}o such file or directory
 
 // Input summary is a broken symlink.
 // RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
 // RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=BROKEN-SYMLINK
-// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '{{.*}}tu-dangling.json': No such file or directory
+// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '{{.*}}tu-dangling.json': {{[Nn]}}o such file or directory

>From 0ff9185d516689364eda8c64847c5f0d70b14c3d Mon Sep 17 00:00:00 2001
From: Aviral Goel <agoel26 at apple.com>
Date: Sat, 7 Mar 2026 22:20:09 -0800
Subject: [PATCH 13/13] Even more fix. Will they ever end?

---
 .../Scalable/ssaf-linker/validation-errors-permissions.test | 6 ++++++
 .../Analysis/Scalable/ssaf-linker/validation-errors.test    | 5 -----
 2 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test
index 801b67b7ba777..adf15243180e6 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors-permissions.test
@@ -11,3 +11,9 @@
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-WRITE-PERM
 // RUN: chmod +w %t/output-dir
 // NO-WRITE-PERM: ssaf-linker: error: failed to validate summary '{{.*}}output.json': Parent directory is not writable
+
+// Input summary is a broken symlink.
+// RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
+// RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=BROKEN-SYMLINK
+// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '{{.*}}tu-dangling.json': {{[Nn]}}o such file or directory
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
index 5a1c37f87f5e2..336d4f45925a5 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/validation-errors.test
@@ -33,8 +33,3 @@
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=NO-INPUT-FILE
 // NO-INPUT-FILE: ssaf-linker: error: failed to validate summary '{{.*}}tu-nonexistent.json': {{[Nn]}}o such file or directory
 
-// Input summary is a broken symlink.
-// RUN: ln -sf %t/tu-nonexistent %t/tu-dangling.json
-// RUN: not ssaf-linker %t/tu-dangling.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=BROKEN-SYMLINK
-// BROKEN-SYMLINK: ssaf-linker: error: failed to validate summary '{{.*}}tu-dangling.json': {{[Nn]}}o such file or directory



More information about the cfe-commits mailing list