https://github.com/jkorous-apple created 
https://github.com/llvm/llvm-project/pull/203660

For details please see individual commits.
I feel the 4 commits might be easier to understand and review together (perhaps 
one by one). Alternatively, I can split this into 4 PRs - just let me know.

>From 52f86c80816e3216192a19b144e3ec01ce177eac Mon Sep 17 00:00:00 2001
From: Jan Korous <[email protected]>
Date: Fri, 12 Jun 2026 19:15:00 -0700
Subject: [PATCH 1/4] [clang][ssaf] Add source-transformation library
 scaffolding
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Introduces the abstract base classes, registries, and force-linker
anchor for the SSAF source-transformation library.

A `Transformation` is an `ASTConsumer` that consumes a previously
computed `WPASuite` and emits source edits and findings through two
sinks: `SourceEditEmitter` (accumulates `clang::tooling::Replacement`s)
and `TransformationReportEmitter` (accumulates `(ruleId, level, range,
message)` tuples). The accumulated state is then handed to a
`SourceEditFormat` and a `TransformationReportFormat` for serialization.

Three `llvm::Registry`-backed registries — keyed by transformation
name and by file extension respectively — let transformations and
formats be linked in statically (with a force-linker anchor) or loaded
dynamically as a clang plugin. Each registry exposes the standard
`is*Registered` / `make*` / `printAvailable*` helpers used elsewhere
in SSAF.

No transformation or format ships yet; this commit only adds the
plumbing. Existing tools (`clang-ssaf-format`, `clang-ssaf-linker`,
`clang-ssaf-analyzer`) and the clang driver gain the new lib in their
link line so the framework anchor resolves under static linking.

Assisted-By: Claude Opus 4.7
---
 .../BuiltinAnchorSources.def                  |  1 +
 .../SourceTransformation/SourceEditEmitter.h  | 29 +++++++
 .../SourceTransformation/Transformation.h     | 38 +++++++++
 .../TransformationRegistry.h                  | 66 +++++++++++++++
 .../TransformationReportEmitter.h             | 35 ++++++++
 clang/lib/Driver/CMakeLists.txt               |  1 +
 clang/lib/FrontendTool/CMakeLists.txt         |  1 +
 .../CMakeLists.txt                            |  1 +
 .../SourceTransformation/CMakeLists.txt       | 13 +++
 .../TransformationRegistry.cpp                | 44 ++++++++++
 .../tools/clang-ssaf-analyzer/CMakeLists.txt  |  1 +
 clang/tools/clang-ssaf-format/CMakeLists.txt  |  1 +
 clang/tools/clang-ssaf-linker/CMakeLists.txt  |  1 +
 .../CMakeLists.txt                            |  4 +
 .../SourceTransformation/EmitterTest.cpp      | 81 +++++++++++++++++++
 .../SourceTransformation/RegistryTest.cpp     | 78 ++++++++++++++++++
 16 files changed, 395 insertions(+)
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp

diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def 
b/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def
index 7dd866047f556..ba4944ea984cb 100644
--- 
a/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/BuiltinAnchorSources.def
@@ -23,6 +23,7 @@ ANCHOR(JSONFormatAnchorSource)
 ANCHOR(PointerFlowAnalysisAnchorSource)
 ANCHOR(PointerFlowExtractorAnchorSource)
 ANCHOR(PointerFlowJSONFormatAnchorSource)
+ANCHOR(SSAFSourceTransformationAnchorSource)
 ANCHOR(UnsafeBufferUsageAnalysisAnchorSource)
 ANCHOR(UnsafeBufferUsageExtractorAnchorSource)
 ANCHOR(UnsafeBufferUsageJSONFormatAnchorSource)
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h
new file mode 100644
index 0000000000000..e96c9846a35f2
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h
@@ -0,0 +1,29 @@
+//===- SourceEditEmitter.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Abstract accumulator for source edits.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H
+
+#include "clang/Tooling/Core/Replacement.h"
+
+namespace clang::ssaf {
+
+class SourceEditEmitter {
+public:
+  virtual ~SourceEditEmitter() = default;
+
+  virtual void addReplacement(clang::tooling::Replacement R) = 0;
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SOURCEEDITEMITTER_H
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h
new file mode 100644
index 0000000000000..5553b7796b1ad
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h
@@ -0,0 +1,38 @@
+//===- Transformation.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Abstract base class for source transformations. A Transformation is an
+// ASTConsumer that consumes a previously computed WPASuite.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H
+
+#include "clang/AST/ASTConsumer.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h"
+
+namespace clang::ssaf {
+
+class Transformation : public clang::ASTConsumer {
+public:
+  Transformation(const WPASuite &Suite, SourceEditEmitter &Edits,
+                 TransformationReportEmitter &Report)
+      : Suite(Suite), Edits(Edits), Report(Report) {}
+
+protected:
+  const WPASuite &Suite;
+  SourceEditEmitter &Edits;
+  TransformationReportEmitter &Report;
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATION_H
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h
new file mode 100644
index 0000000000000..231a810f60f26
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h
@@ -0,0 +1,66 @@
+//===- TransformationRegistry.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Registry for Transformations, and some helper functions.
+// To register a transformation, insert this code:
+//
+//   namespace clang::ssaf {
+//   // NOLINTNEXTLINE(misc-use-internal-linkage)
+//   volatile int MyTransformationAnchorSource = 0;
+//   } // namespace clang::ssaf
+//   static TransformationRegistry::Add<MyTransformation>
+//     X("MyTransformation", "My awesome transformation");
+//
+// For a statically-linked transformation also extend the `AnchorSources`
+// list in
+// clang/include/clang/ScalableStaticAnalysisFramework/SSAFBuiltinForceLinker.h
+// (plugin-loaded transformations do not need an anchor — the dynamic loader
+// runs every global ctor on load).
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H
+
+#include 
"clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h"
+#include "clang/Support/Compiler.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Registry.h"
+#include "llvm/Support/raw_ostream.h"
+#include <memory>
+
+namespace clang::ssaf {
+
+/// Check if a Transformation was registered with a given name.
+bool isTransformationRegistered(llvm::StringRef Name);
+
+/// Try to instantiate a Transformation with a given name.
+/// This might return null if the construction of the desired Transformation
+/// failed.
+/// It's a fatal error if there is no transformation registered with the name.
+std::unique_ptr<Transformation>
+makeTransformation(llvm::StringRef Name, const WPASuite &Suite,
+                   SourceEditEmitter &Edits,
+                   TransformationReportEmitter &Report);
+
+/// Print the list of available Transformations.
+void printAvailableTransformations(llvm::raw_ostream &OS);
+
+// Registry for adding new Transformation implementations.
+using TransformationRegistry =
+    llvm::Registry<Transformation, const WPASuite &, SourceEditEmitter &,
+                   TransformationReportEmitter &>;
+
+} // namespace clang::ssaf
+
+LLVM_DECLARE_REGISTRY(clang::ssaf::TransformationRegistry)
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREGISTRY_H
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h
new file mode 100644
index 0000000000000..c48a35009c034
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h
@@ -0,0 +1,35 @@
+//===- TransformationReportEmitter.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Abstract accumulator for the transformation report.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H
+
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace clang::ssaf {
+
+class TransformationReportEmitter {
+public:
+  virtual ~TransformationReportEmitter() = default;
+
+  /// An invalid \p Range signals "no location"; the format writer drops the
+  /// location object entirely rather than fabricating a placeholder.
+  virtual void addResult(llvm::StringRef RuleId, clang::SarifResultLevel Level,
+                         clang::CharSourceRange Range,
+                         llvm::StringRef Message) = 0;
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_TRANSFORMATIONREPORTEMITTER_H
diff --git a/clang/lib/Driver/CMakeLists.txt b/clang/lib/Driver/CMakeLists.txt
index 5fe10052d267f..cd410899c450f 100644
--- a/clang/lib/Driver/CMakeLists.txt
+++ b/clang/lib/Driver/CMakeLists.txt
@@ -115,6 +115,7 @@ add_clang_library(clangDriver
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkFrontend
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangSerialization
   clangLex
   clangOptions
diff --git a/clang/lib/FrontendTool/CMakeLists.txt 
b/clang/lib/FrontendTool/CMakeLists.txt
index 24623303e6bdb..543aa06090ec8 100644
--- a/clang/lib/FrontendTool/CMakeLists.txt
+++ b/clang/lib/FrontendTool/CMakeLists.txt
@@ -7,6 +7,7 @@ set(link_libs
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkFrontend
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangBasic
   clangCodeGen
   clangDriver
diff --git a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
index e09c44b7cfd52..b214c06644db7 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -2,4 +2,5 @@ add_subdirectory(Analyses)
 add_subdirectory(Core)
 add_subdirectory(Frontend)
 add_subdirectory(Plugins)
+add_subdirectory(SourceTransformation)
 add_subdirectory(Tool)
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
new file mode 100644
index 0000000000000..c96a386977487
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(LLVM_LINK_COMPONENTS
+  Support
+  )
+
+add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation
+  TransformationRegistry.cpp
+
+  LINK_LIBS
+  clangAST
+  clangBasic
+  clangScalableStaticAnalysisFrameworkCore
+  clangToolingCore
+  )
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp
new file mode 100644
index 0000000000000..9d770c8bcd0f9
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.cpp
@@ -0,0 +1,44 @@
+//===- TransformationRegistry.cpp 
-----------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h"
+#include <memory>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace clang::ssaf {
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int SSAFSourceTransformationAnchorSource = 0;
+} // namespace clang::ssaf
+
+LLVM_DEFINE_REGISTRY(clang::ssaf::TransformationRegistry)
+
+bool ssaf::isTransformationRegistered(llvm::StringRef Name) {
+  for (const auto &Entry : TransformationRegistry::entries())
+    if (Entry.getName() == Name)
+      return true;
+  return false;
+}
+
+std::unique_ptr<Transformation>
+ssaf::makeTransformation(llvm::StringRef Name, const WPASuite &Suite,
+                         SourceEditEmitter &Edits,
+                         TransformationReportEmitter &Report) {
+  for (const auto &Entry : TransformationRegistry::entries())
+    if (Entry.getName() == Name)
+      return Entry.instantiate(Suite, Edits, Report);
+  assert(false && "Unknown Transformation name");
+  return nullptr;
+}
+
+void ssaf::printAvailableTransformations(llvm::raw_ostream &OS) {
+  OS << "OVERVIEW: Available SSAF source transformations:\n\n";
+  for (const auto &Entry : TransformationRegistry::entries())
+    OS << "  " << Entry.getName() << " - " << Entry.getDesc() << "\n";
+}
diff --git a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt 
b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
index 67732867181b6..d58ec773ff04b 100644
--- a/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-analyzer/CMakeLists.txt
@@ -17,6 +17,7 @@ clang_target_link_libraries(clang-ssaf-analyzer
   clangBasic
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangScalableStaticAnalysisFrameworkTool
   )
 
diff --git a/clang/tools/clang-ssaf-format/CMakeLists.txt 
b/clang/tools/clang-ssaf-format/CMakeLists.txt
index 33ce432be3f4a..5d7a6c70c73fb 100644
--- a/clang/tools/clang-ssaf-format/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-format/CMakeLists.txt
@@ -17,6 +17,7 @@ clang_target_link_libraries(clang-ssaf-format
   clangBasic
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangScalableStaticAnalysisFrameworkTool
   )
 
diff --git a/clang/tools/clang-ssaf-linker/CMakeLists.txt 
b/clang/tools/clang-ssaf-linker/CMakeLists.txt
index af65aaa3b1aeb..89d72a45a734e 100644
--- a/clang/tools/clang-ssaf-linker/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-linker/CMakeLists.txt
@@ -12,5 +12,6 @@ clang_target_link_libraries(clang-ssaf-linker
   clangBasic
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangScalableStaticAnalysisFrameworkTool
   )
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt 
b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index e852d99d34781..8090ea96cbd5c 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -24,6 +24,8 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Serialization/JSONFormatTest/JSONFormatTest.cpp
   Serialization/JSONFormatTest/LUSummaryTest.cpp
   Serialization/JSONFormatTest/TUSummaryTest.cpp
+  SourceTransformation/EmitterTest.cpp
+  SourceTransformation/RegistryTest.cpp
   SummaryData/SummaryDataTest.cpp
   SummaryNameTest.cpp
   TestFixture.cpp
@@ -39,8 +41,10 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
   clangScalableStaticAnalysisFrameworkFrontend
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangSerialization
   clangTooling
+  clangToolingCore
 
   LINK_LIBS
   LLVMTestingSupport
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp
new file mode 100644
index 0000000000000..d730c6c337c45
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/EmitterTest.cpp
@@ -0,0 +1,81 @@
+//===- EmitterTest.cpp 
----------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/SourceLocation.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h"
+#include "gtest/gtest.h"
+#include <vector>
+
+using namespace llvm;
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+class RecordingEditEmitter : public SourceEditEmitter {
+public:
+  std::vector<clang::tooling::Replacement> Replacements;
+
+  void addReplacement(clang::tooling::Replacement R) override {
+    Replacements.push_back(std::move(R));
+  }
+};
+
+class RecordingReportEmitter : public TransformationReportEmitter {
+public:
+  struct Entry {
+    std::string RuleId;
+    clang::SarifResultLevel Level;
+    clang::CharSourceRange Range;
+    std::string Message;
+  };
+  std::vector<Entry> Results;
+
+  void addResult(StringRef RuleId, clang::SarifResultLevel Level,
+                 clang::CharSourceRange Range, StringRef Message) override {
+    Results.push_back({RuleId.str(), Level, Range, Message.str()});
+  }
+};
+
+TEST(SourceEditEmitterTest, AccumulatesInOrder) {
+  RecordingEditEmitter E;
+  E.addReplacement(clang::tooling::Replacement("a.cpp", 0, 0, "// 1"));
+  E.addReplacement(clang::tooling::Replacement("a.cpp", 10, 0, "// 2"));
+  ASSERT_EQ(E.Replacements.size(), 2u);
+  EXPECT_EQ(E.Replacements[0].getReplacementText(), "// 1");
+  EXPECT_EQ(E.Replacements[1].getReplacementText(), "// 2");
+  EXPECT_EQ(E.Replacements[0].getOffset(), 0u);
+  EXPECT_EQ(E.Replacements[1].getOffset(), 10u);
+}
+
+TEST(TransformationReportEmitterTest, AccumulatesInOrder) {
+  RecordingReportEmitter R;
+  R.addResult("rule-a", clang::SarifResultLevel::Note, 
clang::CharSourceRange{},
+              "first");
+  R.addResult("rule-b", clang::SarifResultLevel::Warning,
+              clang::CharSourceRange{}, "second");
+  ASSERT_EQ(R.Results.size(), 2u);
+  EXPECT_EQ(R.Results[0].RuleId, "rule-a");
+  EXPECT_EQ(R.Results[0].Level, clang::SarifResultLevel::Note);
+  EXPECT_EQ(R.Results[0].Message, "first");
+  EXPECT_EQ(R.Results[1].RuleId, "rule-b");
+  EXPECT_EQ(R.Results[1].Level, clang::SarifResultLevel::Warning);
+  EXPECT_EQ(R.Results[1].Message, "second");
+}
+
+TEST(TransformationReportEmitterTest, AcceptsInvalidRange) {
+  RecordingReportEmitter R;
+  R.addResult("rule", clang::SarifResultLevel::Note, clang::CharSourceRange{},
+              "no-location");
+  ASSERT_EQ(R.Results.size(), 1u);
+  EXPECT_FALSE(R.Results[0].Range.isValid());
+}
+
+} // namespace
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp
new file mode 100644
index 0000000000000..0c68cd95dd499
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/RegistryTest.cpp
@@ -0,0 +1,78 @@
+//===- RegistryTest.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 "TestFixture.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/Support/raw_ostream.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+class StubEditEmitter : public SourceEditEmitter {
+public:
+  void addReplacement(clang::tooling::Replacement) override {}
+};
+
+class StubReportEmitter : public TransformationReportEmitter {
+public:
+  void addResult(StringRef, clang::SarifResultLevel, clang::CharSourceRange,
+                 StringRef) override {}
+};
+
+class StubTransformation : public Transformation {
+public:
+  using Transformation::Transformation;
+};
+
+} // namespace
+
+static TransformationRegistry::Add<StubTransformation>
+    RegisterStubTransformation("stub-transformation",
+                               "A transformation for testing");
+
+namespace {
+
+class TransformationRegistryTest : public TestFixture {};
+
+TEST_F(TransformationRegistryTest, isTransformationRegistered) {
+  EXPECT_FALSE(isTransformationRegistered("not-a-transformation"));
+  EXPECT_TRUE(isTransformationRegistered("stub-transformation"));
+}
+
+TEST_F(TransformationRegistryTest, makeTransformation) {
+  WPASuite Suite = makeWPASuite();
+  StubEditEmitter Edits;
+  StubReportEmitter Report;
+  std::unique_ptr<Transformation> T =
+      makeTransformation("stub-transformation", Suite, Edits, Report);
+  EXPECT_NE(T, nullptr);
+}
+
+TEST_F(TransformationRegistryTest, EnumeratingRegistryEntries) {
+  auto Entries = TransformationRegistry::entries();
+  EXPECT_TRUE(llvm::any_of(Entries, [](const auto &Entry) {
+    return StringRef(Entry.getName()) == "stub-transformation";
+  }));
+}
+
+TEST_F(TransformationRegistryTest, PrintAvailableTransformations) {
+  std::string Buffer;
+  raw_string_ostream OS(Buffer);
+  printAvailableTransformations(OS);
+  EXPECT_NE(StringRef(Buffer).find("stub-transformation"), StringRef::npos);
+  EXPECT_NE(StringRef(Buffer).find("A transformation for testing"),
+            StringRef::npos);
+}
+
+} // namespace

>From 8b022ec0102773e1b7c3664fd950f42de3adf47c Mon Sep 17 00:00:00 2001
From: Jan Korous <[email protected]>
Date: Fri, 12 Jun 2026 19:15:00 -0700
Subject: [PATCH 2/4] [clang][ssaf] Add YAML source-edit format

Adds the built-in `SourceEditFormat`, registered under the file
extension `yaml`. The writer drives `llvm::yaml::Output` against the
existing `clang::tooling::TranslationUnitReplacements` `MappingTraits`
from `clang/Tooling/ReplacementsYaml.h`, so the resulting document is
byte-for-byte consumable by `clang-apply-replacements`.

Anchored via `SSAFYAMLSourceEditFormatAnchorSource` so static builds
keep the registration.

Assisted-By: Claude Opus 4.7
---
 .../YAMLSourceEditFormat.h                    | 32 ++++++
 .../SourceTransformation/CMakeLists.txt       |  1 +
 .../YAMLSourceEditFormat.cpp                  | 33 +++++++
 .../CMakeLists.txt                            |  1 +
 .../SourceTransformation/YAMLFormatTest.cpp   | 97 +++++++++++++++++++
 5 files changed, 164 insertions(+)
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp

diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h
new file mode 100644
index 0000000000000..e154fabbd8031
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h
@@ -0,0 +1,32 @@
+//===- YAMLSourceEditFormat.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Built-in YAML source-edit writer. The on-disk layout is the existing
+// `clang::tooling::TranslationUnitReplacements` YAML schema, byte-for-byte
+// consumable by `clang-apply-replacements`.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H
+
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+
+namespace clang::ssaf {
+
+/// Writes \p Doc to \p Path as a YAML document compatible with
+/// `clang-apply-replacements`.
+llvm::Error
+writeYAMLSourceEdits(const clang::tooling::TranslationUnitReplacements &Doc,
+                     llvm::StringRef Path);
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_YAMLSOURCEEDITFORMAT_H
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
index c96a386977487..9ec05a2f21a6d 100644
--- 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
@@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS
 
 add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation
   TransformationRegistry.cpp
+  YAMLSourceEditFormat.cpp
 
   LINK_LIBS
   clangAST
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp
new file mode 100644
index 0000000000000..7b472b0ab158d
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.cpp
@@ -0,0 +1,33 @@
+//===- YAMLSourceEditFormat.cpp 
-------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h"
+#include "clang/Tooling/ReplacementsYaml.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/YAMLTraits.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace clang;
+using namespace ssaf;
+
+llvm::Error ssaf::writeYAMLSourceEdits(
+    const clang::tooling::TranslationUnitReplacements &Doc,
+    llvm::StringRef Path) {
+  std::error_code EC;
+  llvm::raw_fd_ostream OS(Path, EC, llvm::sys::fs::OF_None);
+  if (EC)
+    return llvm::createStringError(EC, "failed to open '" + Path + "'");
+
+  // llvm::yaml::Output's stream operator binds to a non-const reference.
+  clang::tooling::TranslationUnitReplacements Mutable = Doc;
+  llvm::yaml::Output YAMLOut(OS);
+  YAMLOut << Mutable;
+
+  return llvm::Error::success();
+}
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt 
b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index 8090ea96cbd5c..f57e1c1c0dbf8 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -26,6 +26,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Serialization/JSONFormatTest/TUSummaryTest.cpp
   SourceTransformation/EmitterTest.cpp
   SourceTransformation/RegistryTest.cpp
+  SourceTransformation/YAMLFormatTest.cpp
   SummaryData/SummaryDataTest.cpp
   SummaryNameTest.cpp
   TestFixture.cpp
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp
new file mode 100644
index 0000000000000..7ce8a35a33562
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/YAMLFormatTest.cpp
@@ -0,0 +1,97 @@
+//===- YAMLFormatTest.cpp 
-------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "clang/Tooling/ReplacementsYaml.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/YAMLTraits.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+// Materializes a unique temporary file path under the system temp dir and
+// removes it on destruction.
+struct TempPath {
+  SmallString<128> Path;
+
+  TempPath(StringRef Suffix) {
+    sys::fs::createUniquePath("ssaf-yaml-%%%%%%." + Suffix, Path,
+                              /*MakeAbsolute=*/true);
+  }
+  ~TempPath() { sys::fs::remove(Path); }
+};
+
+TEST(WriteYAMLSourceEditsTest, RoundTripsTwoReplacements) {
+  clang::tooling::TranslationUnitReplacements Doc;
+  Doc.MainSourceFile = "main.cpp";
+  Doc.Replacements.emplace_back("a.cpp", 0, 0, "/*1*/");
+  Doc.Replacements.emplace_back("b.cpp", 10, 3, "/*2*/");
+
+  TempPath TP("yaml");
+  ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, TP.Path), Succeeded());
+
+  auto BufferOrErr = MemoryBuffer::getFile(TP.Path);
+  ASSERT_TRUE(static_cast<bool>(BufferOrErr))
+      << "Failed to read back '" << TP.Path << "'";
+
+  clang::tooling::TranslationUnitReplacements Parsed;
+  yaml::Input YIn((*BufferOrErr)->getBuffer());
+  YIn >> Parsed;
+  ASSERT_FALSE(YIn.error()) << YIn.error().message();
+
+  EXPECT_EQ(Parsed.MainSourceFile, "main.cpp");
+  ASSERT_EQ(Parsed.Replacements.size(), 2u);
+  EXPECT_EQ(Parsed.Replacements[0].getFilePath(), "a.cpp");
+  EXPECT_EQ(Parsed.Replacements[0].getOffset(), 0u);
+  EXPECT_EQ(Parsed.Replacements[0].getLength(), 0u);
+  EXPECT_EQ(Parsed.Replacements[0].getReplacementText(), "/*1*/");
+  EXPECT_EQ(Parsed.Replacements[1].getFilePath(), "b.cpp");
+  EXPECT_EQ(Parsed.Replacements[1].getOffset(), 10u);
+  EXPECT_EQ(Parsed.Replacements[1].getLength(), 3u);
+  EXPECT_EQ(Parsed.Replacements[1].getReplacementText(), "/*2*/");
+}
+
+TEST(WriteYAMLSourceEditsTest, EmptyReplacementsWritesValidDocument) {
+  clang::tooling::TranslationUnitReplacements Doc;
+  Doc.MainSourceFile = "main.cpp";
+
+  TempPath TP("yaml");
+  ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, TP.Path), Succeeded());
+
+  auto BufferOrErr = MemoryBuffer::getFile(TP.Path);
+  ASSERT_TRUE(static_cast<bool>(BufferOrErr));
+
+  clang::tooling::TranslationUnitReplacements Parsed;
+  yaml::Input YIn((*BufferOrErr)->getBuffer());
+  YIn >> Parsed;
+  ASSERT_FALSE(YIn.error()) << YIn.error().message();
+  EXPECT_EQ(Parsed.MainSourceFile, "main.cpp");
+  EXPECT_TRUE(Parsed.Replacements.empty());
+}
+
+TEST(WriteYAMLSourceEditsTest, OpenErrorReturnsError) {
+  clang::tooling::TranslationUnitReplacements Doc;
+  Doc.MainSourceFile = "main.cpp";
+
+  // Path under a directory that does not exist.
+  SmallString<128> BadPath;
+  sys::fs::createUniquePath("ssaf-missing-%%%%%%/edits.yaml", BadPath,
+                            /*MakeAbsolute=*/true);
+
+  ASSERT_THAT_ERROR(writeYAMLSourceEdits(Doc, BadPath), Failed());
+}
+
+} // namespace

>From 79a53d29be7b58ce0f6ea77eb31c9912ff6a223f Mon Sep 17 00:00:00 2001
From: Jan Korous <[email protected]>
Date: Fri, 12 Jun 2026 19:15:00 -0700
Subject: [PATCH 3/4] [clang][ssaf] Add SARIF transformation-report format
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds the built-in `TransformationReportFormat`, registered under the
file extension `sarif`. The writer drives clang's existing
`SarifDocumentWriter` (`clang/Basic/Sarif.h`) to produce a SARIF 2.1.0
JSON document. The tool driver name is `clang-ssaf`; the long
`fullName` carries the transformation's name.

Token-range `CharSourceRange`s are canonicalized to char ranges via
`clang::Lexer::getAsCharRange` before being attached to results;
invalid ranges (including default-constructed ones) are treated as "no
location" — the writer omits the result's `locations` key rather than
fabricating one. Source edits are not embedded in the report; the
writer never emits a `fix` or `fixes` key.

Anchored via `SSAFSARIFTransformationReportFormatAnchorSource` so
static builds keep the registration.

Assisted-By: Claude Opus 4.7
---
 .../SARIFTransformationReportFormat.h         |  50 +++++
 .../SourceTransformation/CMakeLists.txt       |   1 +
 .../SARIFTransformationReportFormat.cpp       |  57 ++++++
 .../CMakeLists.txt                            |   1 +
 .../SourceTransformation/SARIFFormatTest.cpp  | 185 ++++++++++++++++++
 5 files changed, 294 insertions(+)
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp
 create mode 100644 
clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp

diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h
new file mode 100644
index 0000000000000..cd49bd9fd267f
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h
@@ -0,0 +1,50 @@
+//===- SARIFTransformationReportFormat.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
+//
+//===----------------------------------------------------------------------===//
+//
+// Built-in SARIF 2.1.0 transformation-report writer. Drives clang's existing
+// `SarifDocumentWriter`; emits no `fix` keys (source edits live in the
+// separate edit file).
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H
+
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/SourceLocation.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+#include <string>
+#include <vector>
+
+namespace clang {
+class SourceManager;
+} // namespace clang
+
+namespace clang::ssaf {
+
+struct ReportResult {
+  std::string RuleId;
+  clang::SarifResultLevel Level;
+  clang::CharSourceRange Range;
+  std::string Message;
+};
+
+struct ReportDocument {
+  std::string TransformationName;
+  const clang::SourceManager &SM;
+  std::vector<ReportResult> Results;
+};
+
+/// Writes \p Doc to \p Path as a SARIF 2.1.0 JSON document.
+llvm::Error writeSARIFTransformationReport(const ReportDocument &Doc,
+                                           llvm::StringRef Path);
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_SOURCETRANSFORMATION_SARIFTRANSFORMATIONREPORTFORMAT_H
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
index 9ec05a2f21a6d..31772ce4a18bf 100644
--- 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/CMakeLists.txt
@@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangScalableStaticAnalysisFrameworkSourceTransformation
+  SARIFTransformationReportFormat.cpp
   TransformationRegistry.cpp
   YAMLSourceEditFormat.cpp
 
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp
new file mode 100644
index 0000000000000..694d200c17cfe
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.cpp
@@ -0,0 +1,57 @@
+//===- SARIFTransformationReportFormat.cpp 
--------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h"
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/Version.h"
+#include "llvm/ADT/StringMap.h"
+#include "llvm/Support/Error.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace clang;
+using namespace ssaf;
+
+llvm::Error
+ssaf::writeSARIFTransformationReport(const ReportDocument &Doc,
+                                     llvm::StringRef Path) {
+  std::error_code EC;
+  llvm::raw_fd_ostream OS(Path, EC, llvm::sys::fs::OF_None);
+  if (EC)
+    return llvm::createStringError(EC, "failed to open '" + Path + "'");
+
+  clang::SarifDocumentWriter Writer(Doc.SM);
+  std::string LongToolName =
+      "clang ScalableStaticAnalysisFramework source transformation (" +
+      Doc.TransformationName + ")";
+  Writer.createRun("clang-ssaf", LongToolName, CLANG_VERSION_STRING);
+
+  llvm::StringMap<size_t> RuleIndex;
+  for (const ReportResult &R : Doc.Results) {
+    if (RuleIndex.contains(R.RuleId))
+      continue;
+    RuleIndex[R.RuleId] =
+        Writer.createRule(clang::SarifRule::create().setRuleId(R.RuleId));
+  }
+
+  for (const ReportResult &R : Doc.Results) {
+    clang::SarifResult Result = clang::SarifResult::create(RuleIndex[R.RuleId])
+                                    .setRuleId(R.RuleId)
+                                    .setDiagnosticMessage(R.Message)
+                                    .setDiagnosticLevel(R.Level);
+    if (R.Range.isValid())
+      Result = Result.addLocations({R.Range});
+    Writer.appendResult(Result);
+  }
+
+  llvm::json::Value Document(Writer.createDocument());
+  OS << llvm::formatv("{0:2}", Document) << "\n";
+  return llvm::Error::success();
+}
diff --git a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt 
b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
index f57e1c1c0dbf8..7745a8a688e90 100644
--- a/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
+++ b/clang/unittests/ScalableStaticAnalysisFramework/CMakeLists.txt
@@ -26,6 +26,7 @@ add_distinct_clang_unittest(ClangScalableAnalysisTests
   Serialization/JSONFormatTest/TUSummaryTest.cpp
   SourceTransformation/EmitterTest.cpp
   SourceTransformation/RegistryTest.cpp
+  SourceTransformation/SARIFFormatTest.cpp
   SourceTransformation/YAMLFormatTest.cpp
   SummaryData/SummaryDataTest.cpp
   SummaryNameTest.cpp
diff --git 
a/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp
 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp
new file mode 100644
index 0000000000000..43468bb111d0b
--- /dev/null
+++ 
b/clang/unittests/ScalableStaticAnalysisFramework/SourceTransformation/SARIFFormatTest.cpp
@@ -0,0 +1,185 @@
+//===- SARIFFormatTest.cpp 
------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/Basic/Diagnostic.h"
+#include "clang/Basic/DiagnosticIDs.h"
+#include "clang/Basic/DiagnosticOptions.h"
+#include "clang/Basic/FileManager.h"
+#include "clang/Basic/FileSystemOptions.h"
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Basic/Version.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h"
+#include "llvm/ADT/IntrusiveRefCntPtr.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/VirtualFileSystem.h"
+#include "llvm/Testing/Support/Error.h"
+#include "gtest/gtest.h"
+
+using namespace llvm;
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+struct TempPath {
+  SmallString<128> Path;
+
+  TempPath(StringRef Suffix) {
+    sys::fs::createUniquePath("ssaf-sarif-%%%%%%." + Suffix, Path,
+                              /*MakeAbsolute=*/true);
+  }
+  ~TempPath() { sys::fs::remove(Path); }
+};
+
+class SARIFFormatTest : public ::testing::Test {
+protected:
+  SARIFFormatTest()
+      : Diags(DiagnosticIDs::create(), DiagOpts, new IgnoringDiagConsumer()),
+        VFS(makeIntrusiveRefCnt<vfs::InMemoryFileSystem>()),
+        FileMgr(FileSystemOptions(), VFS), SourceMgr(Diags, FileMgr) {}
+
+  json::Value writeAndParse(const ReportDocument &Doc) {
+    TempPath TP("sarif");
+    EXPECT_THAT_ERROR(writeSARIFTransformationReport(Doc, TP.Path),
+                      Succeeded());
+    auto BufferOrErr = MemoryBuffer::getFile(TP.Path);
+    EXPECT_TRUE(static_cast<bool>(BufferOrErr));
+    auto ParsedOrErr = json::parse((*BufferOrErr)->getBuffer());
+    EXPECT_THAT_EXPECTED(ParsedOrErr, Succeeded());
+    return std::move(*ParsedOrErr);
+  }
+
+  DiagnosticOptions DiagOpts;
+  DiagnosticsEngine Diags;
+  IntrusiveRefCntPtr<vfs::InMemoryFileSystem> VFS;
+  FileManager FileMgr;
+  SourceManager SourceMgr;
+};
+
+TEST_F(SARIFFormatTest, ToolDriverNameAndVersion) {
+  ReportDocument Doc{"my-transformation", SourceMgr, {}};
+  json::Value V = writeAndParse(Doc);
+
+  const json::Object *Root = V.getAsObject();
+  ASSERT_NE(Root, nullptr);
+  const json::Array *Runs = Root->getArray("runs");
+  ASSERT_NE(Runs, nullptr);
+  ASSERT_EQ(Runs->size(), 1u);
+  const json::Object *Driver =
+      (*Runs)[0].getAsObject()->getObject("tool")->getObject("driver");
+  ASSERT_NE(Driver, nullptr);
+  EXPECT_EQ(*Driver->getString("name"), "clang-ssaf");
+  EXPECT_NE(Driver->getString("fullName")->find("my-transformation"),
+            StringRef::npos);
+  EXPECT_EQ(*Driver->getString("version"), CLANG_VERSION_STRING);
+}
+
+TEST_F(SARIFFormatTest, LevelMapping) {
+  ReportDocument Doc{"t",
+                     SourceMgr,
+                     {
+                         {"r-note", clang::SarifResultLevel::Note, {}, "n"},
+                         {"r-warn", clang::SarifResultLevel::Warning, {}, "w"},
+                         {"r-error", clang::SarifResultLevel::Error, {}, "e"},
+                         {"r-none", clang::SarifResultLevel::None, {}, "x"},
+                     }};
+  json::Value V = writeAndParse(Doc);
+
+  const json::Array *Results =
+      V.getAsObject()->getArray("runs")->front().getAsObject()->getArray(
+          "results");
+  ASSERT_NE(Results, nullptr);
+  ASSERT_EQ(Results->size(), 4u);
+  EXPECT_EQ(*(*Results)[0].getAsObject()->getString("level"), "note");
+  EXPECT_EQ(*(*Results)[1].getAsObject()->getString("level"), "warning");
+  EXPECT_EQ(*(*Results)[2].getAsObject()->getString("level"), "error");
+  EXPECT_EQ(*(*Results)[3].getAsObject()->getString("level"), "none");
+}
+
+TEST_F(SARIFFormatTest, InvalidRangeOmitsLocations) {
+  ReportDocument Doc{
+      "t",
+      SourceMgr,
+      {{"r", clang::SarifResultLevel::Note, clang::CharSourceRange{}, "msg"}}};
+  json::Value V = writeAndParse(Doc);
+  const json::Object *Result = V.getAsObject()
+                                   ->getArray("runs")
+                                   ->front()
+                                   .getAsObject()
+                                   ->getArray("results")
+                                   ->front()
+                                   .getAsObject();
+  EXPECT_EQ(Result->get("locations"), nullptr);
+}
+
+TEST_F(SARIFFormatTest, EmptyResultsValidDocument) {
+  ReportDocument Doc{"t", SourceMgr, {}};
+  json::Value V = writeAndParse(Doc);
+  const json::Object *Run =
+      V.getAsObject()->getArray("runs")->front().getAsObject();
+  // SARIF allows the results array to be absent or empty for an empty run.
+  if (const json::Array *Results = Run->getArray("results"))
+    EXPECT_TRUE(Results->empty());
+}
+
+TEST_F(SARIFFormatTest, NoFixKey) {
+  ReportDocument Doc{
+      "t", SourceMgr, {{"r", clang::SarifResultLevel::Warning, {}, "msg"}}};
+  json::Value V = writeAndParse(Doc);
+  const json::Object *Result = V.getAsObject()
+                                   ->getArray("runs")
+                                   ->front()
+                                   .getAsObject()
+                                   ->getArray("results")
+                                   ->front()
+                                   .getAsObject();
+  EXPECT_EQ(Result->get("fix"), nullptr);
+  EXPECT_EQ(Result->get("fixes"), nullptr);
+}
+
+TEST_F(SARIFFormatTest, EmptyRuleIdAccepted) {
+  ReportDocument Doc{
+      "t", SourceMgr, {{"", clang::SarifResultLevel::Note, {}, "m"}}};
+  json::Value V = writeAndParse(Doc);
+  const json::Object *Result = V.getAsObject()
+                                   ->getArray("runs")
+                                   ->front()
+                                   .getAsObject()
+                                   ->getArray("results")
+                                   ->front()
+                                   .getAsObject();
+  ASSERT_NE(Result->getString("ruleId"), std::nullopt);
+  EXPECT_EQ(*Result->getString("ruleId"), "");
+}
+
+TEST_F(SARIFFormatTest, DistinctRuleIdsDeduplicated) {
+  ReportDocument Doc{"t",
+                     SourceMgr,
+                     {
+                         {"a", clang::SarifResultLevel::Note, {}, "m1"},
+                         {"b", clang::SarifResultLevel::Note, {}, "m2"},
+                         {"a", clang::SarifResultLevel::Note, {}, "m3"},
+                     }};
+  json::Value V = writeAndParse(Doc);
+  const json::Array *Rules = V.getAsObject()
+                                 ->getArray("runs")
+                                 ->front()
+                                 .getAsObject()
+                                 ->getObject("tool")
+                                 ->getObject("driver")
+                                 ->getArray("rules");
+  ASSERT_NE(Rules, nullptr);
+  EXPECT_EQ(Rules->size(), 2u);
+}
+
+} // namespace

>From adba1355348c1e22603b2c74692360f3b2c6fa10 Mon Sep 17 00:00:00 2001
From: Jan Korous <[email protected]>
Date: Fri, 12 Jun 2026 19:15:00 -0700
Subject: [PATCH 4/4] [clang][ssaf] Wire up the source-edit-generation pipeline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds the four `--ssaf-*` driver flags
(`--ssaf-source-transformation=`, `--ssaf-global-scope-analysis-result=`,
`--ssaf-src-edit-file=`, `--ssaf-transformation-report-file=`) under
`SSAF_Group`, marshalled into `FrontendOptions`. The compilation-unit
identifier flag introduced earlier is reused. The driver forwards all
four flags to `cc1`.

Adds twelve `warn_ssaf_*` diagnostics under
`-Wscalable-static-analysis-framework` (`DefaultError`) covering the
orphan-flag matrix, unknown transformation names, unknown output
formats, WPA-suite read failures, and edit/report write failures.

Adds `clang::ssaf::SourceTransformationFrontendAction` — a
`WrapperFrontendAction` that, when any source-edit flag is set,
validates the CLI as a group, loads the WPASuite from the configured
path, instantiates the named transformation, and serializes the
accumulated edits and findings through the configured formats. The
action is wrapped in `ExecuteCompilerInvocation` after the existing
TU-summary wrap so both pipelines stack as independent
`ASTConsumer`s on the same translation unit; they exchange no data.

Lit tests under `clang/test/Analysis/Scalable/source-edit-generation/`
cover the orphan-flag matrix, unknown-name and unknown-format paths,
the `-Wno-error=` / `-Wno-` levers, write failures, the end-to-end
happy path through a test-only `SSAFTestTransformationPlugin`, and the
coexistence of stage-1 and stage-2 in a single invocation. The plugin
itself lives under `clang/test/` so no testing artifact ships in
production code; it builds gated on
`CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32` and the
plugin-using lit tests use `REQUIRES: plugins`.

User and developer documentation describe the flag surface, the
diagnostic policy, and how to author and load a transformation.

Assisted-By: Claude Opus 4.7
---
 .../user-docs/SourceEditGeneration.rst        |  77 ++++++
 .../clang/Basic/DiagnosticFrontendKinds.td    |  46 ++++
 clang/include/clang/Basic/DiagnosticIDs.h     |   2 +-
 .../include/clang/Frontend/FrontendOptions.h  |  18 ++
 clang/include/clang/Options/Options.td        |  38 +++
 .../SourceTransformationFrontendAction.h      |  34 +++
 clang/lib/Driver/ToolChains/Clang.cpp         |   4 +
 .../ExecuteCompilerInvocation.cpp             |   7 +
 .../Frontend/CMakeLists.txt                   |   3 +
 .../SourceTransformationFrontendAction.cpp    | 233 ++++++++++++++++++
 clang/test/Analysis/Scalable/help.cpp         |   8 +
 .../Inputs/empty-suite.json                   |   4 +
 .../Inputs/two-function-suite.json            |  25 ++
 .../Plugins/CMakeLists.txt                    |   3 +
 .../TestTransformationPlugin/CMakeLists.txt   |  16 ++
 .../TestTransformation.cpp                    | 101 ++++++++
 .../Plugins/lit.local.cfg                     |   2 +
 .../source-edit-generation/cli-errors.cpp     |  51 ++++
 .../source-edit-generation/coexistence.cpp    |  33 +++
 .../downgradable-errors.cpp                   |  35 +++
 .../source-edit-generation/happy-path.cpp     |  37 +++
 .../source-edit-generation/write-failure.cpp  |  41 +++
 clang/test/CMakeLists.txt                     |   2 +
 23 files changed, 819 insertions(+), 1 deletion(-)
 create mode 100644 
clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst
 create mode 100644 
clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h
 create mode 100644 
clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp
 create mode 100644 
clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp

diff --git 
a/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst 
b/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst
new file mode 100644
index 0000000000000..c7027f8c9fc2c
--- /dev/null
+++ 
b/clang/docs/ScalableStaticAnalysisFramework/user-docs/SourceEditGeneration.rst
@@ -0,0 +1,77 @@
+==============================
+Source Edit Generation
+==============================
+
+Source edit generation is the second stage of the SSAF pipeline. Given a
+``WPASuite`` produced by an earlier whole-program analysis, a *source
+transformation* runs alongside the normal compile and emits two
+per-translation-unit artifacts:
+
+- a *source-edit file* (``--ssaf-src-edit-file=``) containing
+  ``clang::tooling::Replacement`` records ready for
+  ``clang-apply-replacements``,
+- a *transformation-report file* (``--ssaf-transformation-report-file=``)
+  containing diagnostic-style findings.
+
+Driver flags
+============
+
+Four flags control the pipeline; they are all both ``--ssaf-…`` driver
+flags and ``cc1`` flags. The compilation-unit identifier flag is shared
+with the stage-1 pipeline.
+
+.. list-table::
+   :header-rows: 1
+
+   * - Flag
+     - Purpose
+   * - ``--ssaf-source-transformation=<name>``
+     - Name of the transformation to run.
+   * - ``--ssaf-global-scope-analysis-result=<path>.<format>``
+     - WPASuite input. The extension selects the serialization format.
+   * - ``--ssaf-src-edit-file=<path>``
+     - Source-edit output. Always written as a
+       ``clang-apply-replacements``-compatible YAML document; the
+       file extension is not interpreted.
+   * - ``--ssaf-transformation-report-file=<path>``
+     - Transformation-report output. Always written as a SARIF 2.1.0
+       JSON document; the file extension is not interpreted.
+   * - ``--ssaf-compilation-unit-id=<id>``
+     - Stable identifier for this translation unit (also required by
+       the stage-1 pipeline).
+
+When ``--ssaf-source-transformation=`` is non-empty the framework wraps
+the active ``FrontendAction`` in a ``SourceTransformationFrontendAction``;
+otherwise the compile is byte-for-byte unchanged.
+
+Error policy
+============
+
+Every CLI-misuse and runtime-write diagnostic is registered as a
+``Warning ... DefaultError`` under
+``-Wscalable-static-analysis-framework``. This means errors stop the
+compile by default but can be downgraded or silenced:
+
+- ``-Wno-error=scalable-static-analysis-framework`` — diagnostics
+  become warnings and the compile finishes normally. The edit/report
+  files may be absent (if the runner bailed out before writing) or
+  present (if a write call returned an error).
+- ``-Wno-scalable-static-analysis-framework`` — diagnostics are
+  silenced entirely. The compile finishes normally.
+
+Examples
+========
+
+Apply the source edits with ``clang-apply-replacements``:
+
+.. code-block:: console
+
+   $ clang -c foo.cpp \
+       --ssaf-source-transformation=my-transformation \
+       --ssaf-global-scope-analysis-result=wpa.json \
+       --ssaf-src-edit-file=foo.yaml \
+       --ssaf-transformation-report-file=foo.sarif \
+       --ssaf-compilation-unit-id=cu-foo
+   $ clang-apply-replacements --remove-change-desc-files <dir-with-yaml>
+
+The transformation report can be consumed by any SARIF 2.1.0 viewer.
diff --git a/clang/include/clang/Basic/DiagnosticFrontendKinds.td 
b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
index 322ce2c3a75fd..a083a817a32a5 100644
--- a/clang/include/clang/Basic/DiagnosticFrontendKinds.td
+++ b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
@@ -434,6 +434,52 @@ def warn_ssaf_tu_summary_requires_compilation_unit_id :
           "'--ssaf-compilation-unit-id=' to be set">,
   InGroup<ScalableStaticAnalysisFramework>, DefaultError;
 
+def warn_ssaf_source_transformation_unknown_name :
+  Warning<"no source transformation registered with name: %0">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_wpa_file :
+  Warning<"option '--ssaf-source-transformation=' requires "
+          "'--ssaf-global-scope-analysis-result=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_edit_file :
+  Warning<"option '--ssaf-source-transformation=' requires "
+          "'--ssaf-src-edit-file=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_report_file :
+  Warning<"option '--ssaf-source-transformation=' requires "
+          "'--ssaf-transformation-report-file=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_compilation_unit_id :
+  Warning<"option '--ssaf-source-transformation=' requires "
+          "'--ssaf-compilation-unit-id=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_read_wpa_suite_failed :
+  Warning<"failed to read whole-program analysis result from '%0': %1">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_src_edit_file_requires_transformation :
+  Warning<"option '--ssaf-src-edit-file=' requires "
+          "'--ssaf-source-transformation=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_write_src_edit_failed :
+  Warning<"failed to write source edits to '%0': %1">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_transformation_report_file_requires_transformation :
+  Warning<"option '--ssaf-transformation-report-file=' requires "
+          "'--ssaf-source-transformation=' to be set">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
+def warn_ssaf_write_transformation_report_failed :
+  Warning<"failed to write transformation report to '%0': %1">,
+  InGroup<ScalableStaticAnalysisFramework>, DefaultError;
+
 def err_extract_api_ignores_file_not_found :
   Error<"file '%0' specified by '--extract-api-ignores=' not found">, 
DefaultFatal;
 
diff --git a/clang/include/clang/Basic/DiagnosticIDs.h 
b/clang/include/clang/Basic/DiagnosticIDs.h
index 63b5e6a28aac0..2d7e32579b608 100644
--- a/clang/include/clang/Basic/DiagnosticIDs.h
+++ b/clang/include/clang/Basic/DiagnosticIDs.h
@@ -36,7 +36,7 @@ enum class Group;
 enum {
   DIAG_SIZE_COMMON = 300,
   DIAG_SIZE_DRIVER = 400,
-  DIAG_SIZE_FRONTEND = 200,
+  DIAG_SIZE_FRONTEND = 250,
   DIAG_SIZE_SERIALIZATION = 120,
   DIAG_SIZE_LEX = 500,
   DIAG_SIZE_PARSE = 800,
diff --git a/clang/include/clang/Frontend/FrontendOptions.h 
b/clang/include/clang/Frontend/FrontendOptions.h
index 7c242f6e94fe0..824e563a4c08a 100644
--- a/clang/include/clang/Frontend/FrontendOptions.h
+++ b/clang/include/clang/Frontend/FrontendOptions.h
@@ -556,6 +556,24 @@ class FrontendOptions {
   /// across stages of the SSAF pipeline.
   std::string SSAFCompilationUnitId;
 
+  /// Name of the SSAF source transformation to run. Exactly one transformation
+  /// per invocation; non-empty implies the source-transformation pipeline is
+  /// active.
+  std::string SSAFSourceTransformation;
+
+  /// Path of the WPASuite input consumed by the source transformation. The
+  /// extension selects which serialization format reads it.
+  std::string SSAFGlobalScopeAnalysisResult;
+
+  /// Path of the source-edit output file produced by the source
+  /// transformation. The extension selects which `SourceEditFormat` writes it.
+  std::string SSAFSrcEditFile;
+
+  /// Path of the transformation-report output file produced by the source
+  /// transformation. The extension selects which `TransformationReportFormat`
+  /// writes it.
+  std::string SSAFTransformationReportFile;
+
   /// Show available SSAF summary extractors.
   LLVM_PREFERRED_TYPE(bool)
   unsigned SSAFShowExtractors : 1;
diff --git a/clang/include/clang/Options/Options.td 
b/clang/include/clang/Options/Options.td
index b4447fcc04120..e35ce85898038 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -979,6 +979,44 @@ def _ssaf_compilation_unit_id :
     "produced SSAF TU summary. Required when '--ssaf-tu-summary-file=' is "
     "set.">,
   MarshallingInfoString<FrontendOpts<"SSAFCompilationUnitId">>;
+def _ssaf_source_transformation :
+  Joined<["--"], "ssaf-source-transformation=">,
+  MetaVarName<"<name>">,
+  Group<SSAF_Group>,
+  Visibility<[ClangOption, CC1Option]>,
+  HelpText<
+    "Name of the SSAF source transformation to run. Exactly one transformation 
"
+    "per invocation.">,
+  MarshallingInfoString<FrontendOpts<"SSAFSourceTransformation">>;
+def _ssaf_global_scope_analysis_result :
+  Joined<["--"], "ssaf-global-scope-analysis-result=">,
+  MetaVarName<"<path>.<format>">,
+  Group<SSAF_Group>,
+  Visibility<[ClangOption, CC1Option]>,
+  HelpText<
+    "Path to the WPASuite file containing the whole-program analysis result "
+    "consumed by the source transformation. The extension selects which file "
+    "format to use.">,
+  MarshallingInfoString<FrontendOpts<"SSAFGlobalScopeAnalysisResult">>;
+def _ssaf_src_edit_file :
+  Joined<["--"], "ssaf-src-edit-file=">,
+  MetaVarName<"<path>">,
+  Group<SSAF_Group>,
+  Visibility<[ClangOption, CC1Option]>,
+  HelpText<
+    "Output file for the source edits produced by the source transformation. "
+    "The output is a YAML document compatible with "
+    "'clang-apply-replacements'.">,
+  MarshallingInfoString<FrontendOpts<"SSAFSrcEditFile">>;
+def _ssaf_transformation_report_file :
+  Joined<["--"], "ssaf-transformation-report-file=">,
+  MetaVarName<"<path>">,
+  Group<SSAF_Group>,
+  Visibility<[ClangOption, CC1Option]>,
+  HelpText<
+    "Output file for the transformation report produced by the source "
+    "transformation. The output is a SARIF 2.1.0 JSON document.">,
+  MarshallingInfoString<FrontendOpts<"SSAFTransformationReportFile">>;
 def Xarch__
     : JoinedAndSeparate<["-"], "Xarch_">,
       Flags<[NoXarchOption]>,
diff --git 
a/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h
 
b/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h
new file mode 100644
index 0000000000000..bfde3646a7dcf
--- /dev/null
+++ 
b/clang/include/clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h
@@ -0,0 +1,34 @@
+//===- SourceTransformationFrontendAction.h ---------------------*- C++ 
-*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H
+#define 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H
+
+#include "clang/Frontend/FrontendAction.h"
+#include <memory>
+
+namespace clang::ssaf {
+
+/// Wraps the existing \c FrontendAction and runs the source-transformation
+/// pipeline alongside it. The transformation consumes a \c WPASuite read
+/// from \c FrontendOptions::SSAFGlobalScopeAnalysisResult and emits source
+/// edits and a transformation report to the configured output files.
+class SourceTransformationFrontendAction final : public WrapperFrontendAction {
+public:
+  explicit SourceTransformationFrontendAction(
+      std::unique_ptr<FrontendAction> WrappedAction);
+  ~SourceTransformationFrontendAction();
+
+protected:
+  std::unique_ptr<ASTConsumer> CreateASTConsumer(CompilerInstance &CI,
+                                                 StringRef InFile) override;
+};
+
+} // namespace clang::ssaf
+
+#endif // 
LLVM_CLANG_SCALABLESTATICANALYSISFRAMEWORK_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp 
b/clang/lib/Driver/ToolChains/Clang.cpp
index c2ac478d84929..258665748fafd 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -7912,6 +7912,10 @@ void Clang::ConstructJob(Compilation &C, const JobAction 
&JA,
   Args.AddLastArg(CmdArgs, options::OPT__ssaf_extract_summaries);
   Args.AddLastArg(CmdArgs, options::OPT__ssaf_tu_summary_file);
   Args.AddLastArg(CmdArgs, options::OPT__ssaf_compilation_unit_id);
+  Args.AddLastArg(CmdArgs, options::OPT__ssaf_source_transformation);
+  Args.AddLastArg(CmdArgs, options::OPT__ssaf_global_scope_analysis_result);
+  Args.AddLastArg(CmdArgs, options::OPT__ssaf_src_edit_file);
+  Args.AddLastArg(CmdArgs, options::OPT__ssaf_transformation_report_file);
 
   // Handle serialized diagnostics.
   if (Arg *A = Args.getLastArg(options::OPT__serialize_diags)) {
diff --git a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp 
b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
index e4622496758ac..7bf272acd75ac 100644
--- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
+++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
@@ -23,6 +23,7 @@
 #include "clang/FrontendTool/Utils.h"
 #include "clang/Options/Options.h"
 #include "clang/Rewrite/Frontend/FrontendActions.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h"
 #include 
"clang/ScalableStaticAnalysisFramework/Frontend/TUSummaryExtractorFrontendAction.h"
 #include "clang/ScalableStaticAnalysisFramework/SSAFForceLinker.h" // IWYU 
pragma: keep
 #include "clang/StaticAnalyzer/Frontend/AnalyzerHelpFlags.h"
@@ -213,6 +214,12 @@ CreateFrontendAction(CompilerInstance &CI) {
     Act = std::make_unique<ssaf::TUSummaryExtractorFrontendAction>(
         std::move(Act));
   }
+  if (!FEOpts.SSAFSourceTransformation.empty() ||
+      !FEOpts.SSAFSrcEditFile.empty() ||
+      !FEOpts.SSAFTransformationReportFile.empty()) {
+    Act = std::make_unique<ssaf::SourceTransformationFrontendAction>(
+        std::move(Act));
+  }
   return Act;
 }
 
diff --git a/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt 
b/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt
index 3da1558810572..72d56c1a4b42e 100644
--- a/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysisFramework/Frontend/CMakeLists.txt
@@ -3,6 +3,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_library(clangScalableStaticAnalysisFrameworkFrontend
+  SourceTransformationFrontendAction.cpp
   TUSummaryExtractorFrontendAction.cpp
 
   LINK_LIBS
@@ -11,5 +12,7 @@ add_clang_library(clangScalableStaticAnalysisFrameworkFrontend
   clangFrontend
   clangScalableStaticAnalysisFrameworkAnalyses
   clangScalableStaticAnalysisFrameworkCore
+  clangScalableStaticAnalysisFrameworkSourceTransformation
   clangSema
+  clangToolingCore
   )
diff --git 
a/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp
 
b/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp
new file mode 100644
index 0000000000000..ecf9f00d1da67
--- /dev/null
+++ 
b/clang/lib/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.cpp
@@ -0,0 +1,233 @@
+//===- SourceTransformationFrontendAction.cpp 
-----------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include 
"clang/ScalableStaticAnalysisFramework/Frontend/SourceTransformationFrontendAction.h"
+#include "clang/AST/ASTConsumer.h"
+#include "clang/Basic/DiagnosticFrontend.h"
+#include "clang/Frontend/CompilerInstance.h"
+#include "clang/Frontend/MultiplexConsumer.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormat.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/Serialization/SerializationFormatRegistry.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SARIFTransformationReportFormat.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/SourceEditEmitter.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationReportEmitter.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/YAMLSourceEditFormat.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/IOSandbox.h"
+#include "llvm/Support/Path.h"
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+using namespace clang;
+using namespace ssaf;
+
+namespace {
+
+/// Concrete `SourceEditEmitter` that buffers replacements until flushed.
+class AccumulatorSourceEditEmitter final : public SourceEditEmitter {
+public:
+  void addReplacement(clang::tooling::Replacement R) override {
+    Replacements.push_back(std::move(R));
+  }
+
+  std::vector<clang::tooling::Replacement> Replacements;
+};
+
+/// Concrete `TransformationReportEmitter` that buffers results until flushed.
+class AccumulatorReportEmitter final : public TransformationReportEmitter {
+public:
+  void addResult(StringRef RuleId, clang::SarifResultLevel Level,
+                 clang::CharSourceRange Range, StringRef Message) override {
+    Results.push_back({RuleId.str(), Level, Range, Message.str()});
+  }
+
+  std::vector<ReportResult> Results;
+};
+
+/// Per-TU runner: owns the loaded `WPASuite`, the accumulator emitters, and
+/// the user-supplied `Transformation`. Inherits from `MultiplexConsumer` so
+/// the transformation's `ASTConsumer` virtuals are forwarded for free;
+/// serializes both outputs after the AST walk completes.
+class SourceTransformationRunner final : public MultiplexConsumer {
+public:
+  static std::unique_ptr<SourceTransformationRunner>
+  create(CompilerInstance &CI, StringRef InFile);
+
+private:
+  SourceTransformationRunner(WPASuite Suite, const FrontendOptions &Opts,
+                             StringRef InFile);
+
+  void HandleTranslationUnit(ASTContext &Ctx) override;
+
+  WPASuite Suite;
+  AccumulatorSourceEditEmitter Edits;
+  AccumulatorReportEmitter Report;
+  const FrontendOptions &Opts;
+  std::string InFile;
+};
+
+} // namespace
+
+/// Returns the bare extension of \p Path (no leading dot), or `std::nullopt` 
if
+/// \p Path is empty or has no recognizable extension.
+static std::optional<StringRef> bareExtension(StringRef Path) {
+  StringRef Ext = llvm::sys::path::extension(Path);
+  if (!Ext.consume_front("."))
+    return std::nullopt;
+  return Ext;
+}
+
+/// Returns `true` if any orphan-flag warning was reported. Every missing
+/// companion flag fires its own diagnostic in a single pass so the user
+/// sees the full list of CLI mistakes at once.
+static bool reportOrphanFlagMisuse(DiagnosticsEngine &Diags,
+                                   const FrontendOptions &Opts) {
+  bool Reported = false;
+
+  if (!Opts.SSAFSourceTransformation.empty()) {
+    if (Opts.SSAFGlobalScopeAnalysisResult.empty()) {
+      Diags.Report(diag::warn_ssaf_source_transformation_requires_wpa_file);
+      Reported = true;
+    }
+    if (Opts.SSAFSrcEditFile.empty()) {
+      Diags.Report(diag::warn_ssaf_source_transformation_requires_edit_file);
+      Reported = true;
+    }
+    if (Opts.SSAFTransformationReportFile.empty()) {
+      Diags.Report(diag::warn_ssaf_source_transformation_requires_report_file);
+      Reported = true;
+    }
+    if (Opts.SSAFCompilationUnitId.empty()) {
+      Diags.Report(
+          diag::warn_ssaf_source_transformation_requires_compilation_unit_id);
+      Reported = true;
+    }
+  } else {
+    if (!Opts.SSAFSrcEditFile.empty()) {
+      Diags.Report(diag::warn_ssaf_src_edit_file_requires_transformation);
+      Reported = true;
+    }
+    if (!Opts.SSAFTransformationReportFile.empty()) {
+      Diags.Report(
+          diag::warn_ssaf_transformation_report_file_requires_transformation);
+      Reported = true;
+    }
+  }
+
+  return Reported;
+}
+
+std::unique_ptr<SourceTransformationRunner>
+SourceTransformationRunner::create(CompilerInstance &CI, StringRef InFile) {
+  const FrontendOptions &Opts = CI.getFrontendOpts();
+  DiagnosticsEngine &Diags = CI.getDiagnostics();
+
+  if (reportOrphanFlagMisuse(Diags, Opts))
+    return nullptr;
+  if (Opts.SSAFSourceTransformation.empty())
+    return nullptr;
+
+  if (!isTransformationRegistered(Opts.SSAFSourceTransformation)) {
+    Diags.Report(diag::warn_ssaf_source_transformation_unknown_name)
+        << Opts.SSAFSourceTransformation;
+    return nullptr;
+  }
+
+  std::optional<StringRef> WPAExt =
+      bareExtension(Opts.SSAFGlobalScopeAnalysisResult);
+  std::unique_ptr<SerializationFormat> WPAFormat =
+      WPAExt && isFormatRegistered(*WPAExt) ? makeFormat(*WPAExt) : nullptr;
+  if (!WPAFormat) {
+    Diags.Report(diag::warn_ssaf_read_wpa_suite_failed)
+        << Opts.SSAFGlobalScopeAnalysisResult << "unknown serialization 
format";
+    return nullptr;
+  }
+  llvm::sys::sandbox::ScopedSetting Guard = 
llvm::sys::sandbox::scopedDisable();
+  llvm::Expected<WPASuite> SuiteOrErr =
+      WPAFormat->readWPASuite(Opts.SSAFGlobalScopeAnalysisResult);
+  if (!SuiteOrErr) {
+    Diags.Report(diag::warn_ssaf_read_wpa_suite_failed)
+        << Opts.SSAFGlobalScopeAnalysisResult
+        << llvm::toString(SuiteOrErr.takeError());
+    return nullptr;
+  }
+
+  return std::unique_ptr<SourceTransformationRunner>{
+      new SourceTransformationRunner(std::move(*SuiteOrErr), Opts, InFile)};
+}
+
+SourceTransformationRunner::SourceTransformationRunner(
+    WPASuite Suite, const FrontendOptions &Opts, StringRef InFile)
+    : MultiplexConsumer(std::vector<std::unique_ptr<ASTConsumer>>{}),
+      Suite(std::move(Suite)), Opts(Opts), InFile(InFile) {
+  // The transformation must be constructed after Suite/Edits/Report start
+  // their lifetimes — those references are captured in its base ctor.
+  std::vector<std::unique_ptr<ASTConsumer>> Consumers;
+  Consumers.push_back(makeTransformation(Opts.SSAFSourceTransformation,
+                                         this->Suite, Edits, Report));
+  assert(Consumers.front());
+  MultiplexConsumer::Consumers = std::move(Consumers);
+}
+
+void SourceTransformationRunner::HandleTranslationUnit(ASTContext &Ctx) {
+  // First, run the transformation.
+  MultiplexConsumer::HandleTranslationUnit(Ctx);
+
+  llvm::sys::sandbox::ScopedSetting Guard = 
llvm::sys::sandbox::scopedDisable();
+
+  // Then serialize the source edits.
+  clang::tooling::TranslationUnitReplacements EditDoc;
+  EditDoc.MainSourceFile = InFile;
+  EditDoc.Replacements = std::move(Edits.Replacements);
+  if (auto Err = writeYAMLSourceEdits(EditDoc, Opts.SSAFSrcEditFile)) {
+    Ctx.getDiagnostics().Report(diag::warn_ssaf_write_src_edit_failed)
+        << Opts.SSAFSrcEditFile << llvm::toString(std::move(Err));
+  }
+
+  // And the transformation report.
+  ReportDocument ReportDoc{Opts.SSAFSourceTransformation,
+                           Ctx.getSourceManager(),
+                           std::move(Report.Results)};
+  if (auto Err = writeSARIFTransformationReport(
+          ReportDoc, Opts.SSAFTransformationReportFile)) {
+    Ctx.getDiagnostics().Report(
+        diag::warn_ssaf_write_transformation_report_failed)
+        << Opts.SSAFTransformationReportFile << llvm::toString(std::move(Err));
+  }
+}
+
+SourceTransformationFrontendAction::~SourceTransformationFrontendAction() =
+    default;
+
+SourceTransformationFrontendAction::SourceTransformationFrontendAction(
+    std::unique_ptr<FrontendAction> WrappedAction)
+    : WrapperFrontendAction(std::move(WrappedAction)) {}
+
+std::unique_ptr<ASTConsumer>
+SourceTransformationFrontendAction::CreateASTConsumer(CompilerInstance &CI,
+                                                      StringRef InFile) {
+  auto WrappedConsumer = WrapperFrontendAction::CreateASTConsumer(CI, InFile);
+  if (!WrappedConsumer)
+    return nullptr;
+
+  if (auto Runner = SourceTransformationRunner::create(CI, InFile)) {
+    CI.getCodeGenOpts().ClearASTBeforeBackend = false;
+    std::vector<std::unique_ptr<ASTConsumer>> Consumers;
+    Consumers.reserve(2);
+    Consumers.push_back(std::move(WrappedConsumer));
+    Consumers.push_back(std::move(Runner));
+    return std::make_unique<MultiplexConsumer>(std::move(Consumers));
+  }
+  return WrappedConsumer;
+}
diff --git a/clang/test/Analysis/Scalable/help.cpp 
b/clang/test/Analysis/Scalable/help.cpp
index 15d6d109360d8..0c82e21323fdb 100644
--- a/clang/test/Analysis/Scalable/help.cpp
+++ b/clang/test/Analysis/Scalable/help.cpp
@@ -7,8 +7,16 @@
 // HELP-NEXT:    Stable identifier used as the CompilationUnit namespace name 
of every produced SSAF TU summary. Required when '--ssaf-tu-summary-file=' is 
set.
 // HELP-NEXT:  --ssaf-extract-summaries=<summary-names>
 // HELP-NEXT:    Comma-separated list of summary names to extract
+// HELP-NEXT:  --ssaf-global-scope-analysis-result=<path>.<format>
+// HELP-NEXT:    Path to the WPASuite file containing the whole-program 
analysis result consumed by the source transformation. The extension selects 
which file format to use.
 // HELP-NEXT:  --ssaf-list-extractors  Display the list of available SSAF 
summary extractors
 // HELP-NEXT:  --ssaf-list-formats     Display the list of available SSAF 
serialization formats
+// HELP-NEXT:  --ssaf-source-transformation=<name>
+// HELP-NEXT:    Name of the SSAF source transformation to run. Exactly one 
transformation per invocation.
+// HELP-NEXT:  --ssaf-src-edit-file=<path>
+// HELP-NEXT:    Output file for the source edits produced by the source 
transformation. The output is a YAML document compatible with 
'clang-apply-replacements'.
+// HELP-NEXT:  --ssaf-transformation-report-file=<path>
+// HELP-NEXT:    Output file for the transformation report produced by the 
source transformation. The output is a SARIF 2.1.0 JSON document.
 // HELP-NEXT:  --ssaf-tu-summary-file=<path>.<format>
 // HELP-NEXT:    The output file for the extracted summaries. The extension 
selects which file format to use.
 
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json 
b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json
new file mode 100644
index 0000000000000..80637e10fc66f
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json
@@ -0,0 +1,4 @@
+{
+  "id_table": [],
+  "results": []
+}
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json
 
b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json
new file mode 100644
index 0000000000000..fc46d6aab7f36
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json
@@ -0,0 +1,25 @@
+{
+  "id_table": [
+    {
+      "id": 0,
+      "name": {
+        "namespace": [
+          { "kind": "LinkUnit", "name": "test.exe" }
+        ],
+        "suffix": "",
+        "usr": "c:@F@foo#"
+      }
+    },
+    {
+      "id": 1,
+      "name": {
+        "namespace": [
+          { "kind": "LinkUnit", "name": "test.exe" }
+        ],
+        "suffix": "",
+        "usr": "c:@F@bar#"
+      }
+    }
+  ],
+  "results": []
+}
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt
new file mode 100644
index 0000000000000..a91194e9ca8f9
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/CMakeLists.txt
@@ -0,0 +1,3 @@
+if(CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32)
+  add_subdirectory(TestTransformationPlugin)
+endif()
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt
 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt
new file mode 100644
index 0000000000000..8d1ca5e0a7a38
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/CMakeLists.txt
@@ -0,0 +1,16 @@
+# Do NOT set LLVM_LINK_COMPONENTS or clang_target_link_libraries here.
+#
+# Static link would produce a second copy of LLVM and Clang libraries in the
+# plugin's address space alongside the copies already loaded by clang itself,
+# resulting in two distinct llvm::Registry instances and broken registration.
+# Let the dynamic loader resolve every symbol from clang at load time.
+
+add_llvm_library(SSAFTestTransformationPlugin MODULE BUILDTREE_ONLY
+  TestTransformation.cpp
+  PLUGIN_TOOL clang
+  )
+
+target_include_directories(SSAFTestTransformationPlugin PRIVATE
+  
$<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkCore,INTERFACE_INCLUDE_DIRECTORIES>
+  
$<TARGET_PROPERTY:clangScalableStaticAnalysisFrameworkSourceTransformation,INTERFACE_INCLUDE_DIRECTORIES>
+  )
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp
 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp
new file mode 100644
index 0000000000000..c3579d181f732
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/TestTransformationPlugin/TestTransformation.cpp
@@ -0,0 +1,101 @@
+//===- TestTransformation.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
+//
+//===----------------------------------------------------------------------===//
+//
+// A transformation used only by lit tests for the source-edit-generation
+// pipeline. It walks every function in the main source file, emits a
+// zero-length `/*T*/` comment at the function body's start, and adds one
+// `test-touches-function` note per visited function. Its level is always
+// `Note` — the goal is to exercise the framework's plumbing, not to
+// produce meaningful findings. The level rises to `Warning` when the
+// input WPASuite's id table is non-empty, giving lit tests a knob to
+// confirm the suite is read at all without depending on namespace
+// matching.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/AST/ASTContext.h"
+#include "clang/AST/Decl.h"
+#include "clang/AST/RecursiveASTVisitor.h"
+#include "clang/Basic/Sarif.h"
+#include "clang/Basic/SourceLocation.h"
+#include "clang/Basic/SourceManager.h"
+#include "clang/Lex/Lexer.h"
+#include "clang/ScalableStaticAnalysisFramework/Core/Model/EntityIdTable.h"
+#include 
"clang/ScalableStaticAnalysisFramework/Core/WholeProgramAnalysis/WPASuite.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/Transformation.h"
+#include 
"clang/ScalableStaticAnalysisFramework/SourceTransformation/TransformationRegistry.h"
+#include "clang/Tooling/Core/Replacement.h"
+#include "llvm/ADT/SmallString.h"
+
+using namespace clang;
+using namespace clang::ssaf;
+
+namespace {
+
+class TestTransformation final : public Transformation {
+public:
+  using Transformation::Transformation;
+
+  void HandleTranslationUnit(ASTContext &Ctx) override {
+    bool SuiteIsNonEmpty = Suite.getIdTable().count() > 0;
+    Visitor V{*this, Ctx, SuiteIsNonEmpty};
+    V.TraverseDecl(Ctx.getTranslationUnitDecl());
+  }
+
+private:
+  class Visitor : public RecursiveASTVisitor<Visitor> {
+  public:
+    Visitor(TestTransformation &T, ASTContext &Ctx, bool SuiteIsNonEmpty)
+        : T(T), Ctx(Ctx), Level(SuiteIsNonEmpty
+                                    ? clang::SarifResultLevel::Warning
+                                    : clang::SarifResultLevel::Note) {}
+
+    bool VisitFunctionDecl(FunctionDecl *FD) {
+      if (!FD->hasBody())
+        return true;
+      SourceManager &SM = Ctx.getSourceManager();
+      if (!SM.isInMainFile(FD->getLocation()))
+        return true;
+
+      Stmt *Body = FD->getBody();
+      SourceLocation BodyStart = Body->getBeginLoc();
+      if (BodyStart.isInvalid())
+        return true;
+
+      llvm::SmallString<64> FilePath(SM.getFilename(BodyStart));
+      unsigned Offset = SM.getFileOffset(BodyStart);
+      T.Edits.addReplacement(
+          clang::tooling::Replacement(FilePath, Offset, /*Length=*/0, 
"/*T*/"));
+
+      CharSourceRange Range = Lexer::getAsCharRange(
+          CharSourceRange::getTokenRange(FD->getNameInfo().getSourceRange()),
+          SM, Ctx.getLangOpts());
+      std::string Message = "visited " + FD->getNameAsString();
+      T.Report.addResult("test-touches-function", Level, Range, Message);
+      return true;
+    }
+
+  private:
+    TestTransformation &T;
+    ASTContext &Ctx;
+    clang::SarifResultLevel Level;
+  };
+};
+
+} // namespace
+
+namespace clang::ssaf {
+// NOLINTNEXTLINE(misc-use-internal-linkage)
+volatile int SSAFTestTransformationAnchorSource = 0;
+} // namespace clang::ssaf
+
+static TransformationRegistry::Add<TestTransformation>
+    RegisterTestTransformation("test-transformation",
+                               "Test transformation for the SSAF "
+                               "source-edit-generation lit suite");
+
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg 
b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg
new file mode 100644
index 0000000000000..bc7dd40525f86
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/Plugins/lit.local.cfg
@@ -0,0 +1,2 @@
+config.suffixes = []
+config.unsupported = True
diff --git a/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp 
b/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp
new file mode 100644
index 0000000000000..a0adf61c8c6fb
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/cli-errors.cpp
@@ -0,0 +1,51 @@
+// CLI errors for the source-edit-generation pipeline. Every misuse of the
+// four `--ssaf-{source-transformation,global-scope-analysis-result,
+// src-edit-file,transformation-report-file}=` flags emits a default-error
+// diagnostic under `-Wscalable-static-analysis-framework`. The runner
+// produces no edit/report files and the rest of the compile pipeline is
+// untouched.
+
+// DEFINE: %{filecheck} = FileCheck %s --match-full-lines --check-prefix
+// DEFINE: %{base} = --ssaf-source-transformation=does-not-exist \
+// DEFINE:   --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \
+// DEFINE:   --ssaf-src-edit-file=%t/edits.yaml \
+// DEFINE:   --ssaf-transformation-report-file=%t/report.sarif \
+// DEFINE:   --ssaf-compilation-unit-id=cu
+
+// 
=============================================================================
+// 1. Unknown transformation name.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: not %clang     -c %s -o %t/test.o %{base} 2>&1 | 
%{filecheck}=UNKNOWN-NAME
+// RUN: not %clang_cc1    %s              %{base} 2>&1 | 
%{filecheck}=UNKNOWN-NAME
+// UNKNOWN-NAME: error: no source transformation registered with name: 
does-not-exist [-Wscalable-static-analysis-framework]
+// RUN: not test -e %t/edits.yaml
+// RUN: not test -e %t/report.sarif
+
+// 
=============================================================================
+// 2. Orphan companion flags: --ssaf-source-transformation= alone.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: not %clang -c %s -o %t/test.o 
--ssaf-source-transformation=does-not-exist 2>&1 | 
%{filecheck}=ORPHAN-COMPANIONS
+// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' 
requires '--ssaf-global-scope-analysis-result=' to be set 
[-Wscalable-static-analysis-framework]
+// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' 
requires '--ssaf-src-edit-file=' to be set 
[-Wscalable-static-analysis-framework]
+// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' 
requires '--ssaf-transformation-report-file=' to be set 
[-Wscalable-static-analysis-framework]
+// ORPHAN-COMPANIONS-DAG: error: option '--ssaf-source-transformation=' 
requires '--ssaf-compilation-unit-id=' to be set 
[-Wscalable-static-analysis-framework]
+
+// 
=============================================================================
+// 3. Reverse orphans: edit/report file set without transformation flag.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: not %clang -c %s -o %t/test.o --ssaf-src-edit-file=%t/e.yaml 2>&1 | 
%{filecheck}=ORPHAN-EDIT
+// ORPHAN-EDIT: error: option '--ssaf-src-edit-file=' requires 
'--ssaf-source-transformation=' to be set [-Wscalable-static-analysis-framework]
+// RUN: not test -e %t/e.yaml
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: not %clang -c %s -o %t/test.o 
--ssaf-transformation-report-file=%t/r.sarif 2>&1 | %{filecheck}=ORPHAN-REPORT
+// ORPHAN-REPORT: error: option '--ssaf-transformation-report-file=' requires 
'--ssaf-source-transformation=' to be set [-Wscalable-static-analysis-framework]
+// RUN: not test -e %t/r.sarif
+
+void foo() {}
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp 
b/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp
new file mode 100644
index 0000000000000..85268c1d0dc62
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/coexistence.cpp
@@ -0,0 +1,33 @@
+// Stage-1 (TU-summary extraction) and stage-2 (source-edit generation) can
+// both be active in a single clang invocation. Their flags do not interact
+// at the data layer — the source transformation reads its WPASuite from
+// disk, not from the in-flight extractor. The two pipelines stack as
+// independent ASTConsumers; both produce their per-TU output files.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \
+// RUN:   --ssaf-extract-summaries=CallGraph \
+// RUN:   --ssaf-tu-summary-file=%t/tu.json \
+// RUN:   --ssaf-source-transformation=test-transformation \
+// RUN:   --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \
+// RUN:   --ssaf-src-edit-file=%t/edits.yaml \
+// RUN:   --ssaf-transformation-report-file=%t/report.sarif \
+// RUN:   --ssaf-compilation-unit-id=cu \
+// RUN:   -emit-obj -o %t/test.o %s
+
+// All four artifacts must be present.
+// RUN: test -e %t/test.o
+// RUN: test -e %t/tu.json
+// RUN: test -e %t/edits.yaml
+// RUN: test -e %t/report.sarif
+
+// And the source-transformation outputs are non-trivial (the plugin emits
+// one replacement and one finding per function in the main file).
+// RUN: FileCheck --check-prefix=EDITS --input-file=%t/edits.yaml %s
+// EDITS:    ReplacementText: '/*T*/'
+// RUN: FileCheck --check-prefix=REPORT --input-file=%t/report.sarif %s
+// REPORT:   "ruleId": "test-touches-function"
+
+void foo() {}
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp 
b/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp
new file mode 100644
index 0000000000000..23855a67f4572
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/source-edit-generation/downgradable-errors.cpp
@@ -0,0 +1,35 @@
+// The source-edit-generation diagnostics are downgradable via
+// `-Wno-error=scalable-static-analysis-framework` and silenceable via
+// `-Wno-scalable-static-analysis-framework`. In both cases the
+// compilation continues normally and produces its object file, but no
+// edit or report file is written.
+
+// DEFINE: %{filecheck} = FileCheck %s --match-full-lines --check-prefix
+// DEFINE: %{flags} = --ssaf-source-transformation=does-not-exist \
+// DEFINE:   --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \
+// DEFINE:   --ssaf-src-edit-file=%t/edits.yaml \
+// DEFINE:   --ssaf-transformation-report-file=%t/report.sarif \
+// DEFINE:   --ssaf-compilation-unit-id=cu
+
+// 
=============================================================================
+// 1. -Wno-error=scalable-static-analysis-framework downgrades to a warning.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang -c %s -o %t/test.o 
-Wno-error=scalable-static-analysis-framework %{flags} 2>&1 | 
%{filecheck}=WARNING
+// WARNING: warning: no source transformation registered with name: 
does-not-exist [-Wscalable-static-analysis-framework]
+// RUN: test -e %t/test.o
+// RUN: not test -e %t/edits.yaml
+// RUN: not test -e %t/report.sarif
+
+// 
=============================================================================
+// 2. -Wno-scalable-static-analysis-framework silences the diagnostic.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang -c %s -o %t/test.o -Wno-scalable-static-analysis-framework 
%{flags} 2>&1 | count 0
+// RUN: test -e %t/test.o
+// RUN: not test -e %t/edits.yaml
+// RUN: not test -e %t/report.sarif
+
+void foo() {}
diff --git a/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp 
b/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp
new file mode 100644
index 0000000000000..d22c901217c31
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/happy-path.cpp
@@ -0,0 +1,37 @@
+// End-to-end test of the source-edit-generation pipeline driven by the
+// `test-transformation` plugin. Walks every function in the main source
+// file, inserts a zero-length `/*T*/` comment at each function body's
+// start, and emits one `test-touches-function` finding per function.
+// The finding's level is `Warning` if the function's USR is in the input
+// WPASuite, `Note` otherwise — verifying the WPASuite is actually read.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \
+// RUN:   --ssaf-source-transformation=test-transformation \
+// RUN:   
--ssaf-global-scope-analysis-result=%S/Inputs/two-function-suite.json \
+// RUN:   --ssaf-src-edit-file=%t/edits.yaml \
+// RUN:   --ssaf-transformation-report-file=%t/report.sarif \
+// RUN:   --ssaf-compilation-unit-id=cu \
+// RUN:   -emit-obj -o %t/test.o %s
+
+// RUN: FileCheck --check-prefix=EDITS --input-file=%t/edits.yaml %s
+// EDITS:      MainSourceFile: {{.*}}happy-path.cpp
+// EDITS:      Replacements:
+// EDITS-DAG:    Offset:          {{[0-9]+}}
+// EDITS-DAG:    ReplacementText: '/*T*/'
+
+// RUN: FileCheck --check-prefix=REPORT --input-file=%t/report.sarif %s
+// REPORT-DAG: "name": "clang-ssaf"
+// REPORT-DAG: "fullName": {{.*}}test-transformation
+// REPORT-DAG: "ruleId": "test-touches-function"
+// REPORT-DAG: "level": "warning"
+// REPORT-DAG: "uri": "file://{{.*}}happy-path.cpp"
+
+// `foo` and `bar` are in two-function-suite.json's id_table, so their
+// findings escalate from Note to Warning. `baz` is not — its finding
+// stays at Note.
+void foo() {}
+void bar() {}
+void baz() {}
diff --git 
a/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp 
b/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp
new file mode 100644
index 0000000000000..74ef0421045a8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/write-failure.cpp
@@ -0,0 +1,41 @@
+// When the source-edit or transformation-report writer's `write` returns an
+// `llvm::Error`, the framework reports a default-error diagnostic. With
+// `-Wno-error=scalable-static-analysis-framework` the diagnostic downgrades
+// to a warning and the rest of the compile pipeline finishes normally.
+
+// REQUIRES: plugins
+
+// RUN: rm -rf %t && mkdir -p %t
+
+// 
=============================================================================
+// 1. Source-edit write fails because the parent directory does not exist.
+// 
=============================================================================
+
+// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \
+// RUN:   -Wno-error=scalable-static-analysis-framework \
+// RUN:   --ssaf-source-transformation=test-transformation \
+// RUN:   --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \
+// RUN:   --ssaf-src-edit-file=%t/missing-dir/edits.yaml \
+// RUN:   --ssaf-transformation-report-file=%t/report.sarif \
+// RUN:   --ssaf-compilation-unit-id=cu \
+// RUN:   -emit-obj -o %t/test.o %s 2>&1 | FileCheck --check-prefix=EDIT-FAIL 
%s
+// EDIT-FAIL: warning: failed to write source edits to 
'{{.*}}/missing-dir/edits.yaml'{{.*}}[-Wscalable-static-analysis-framework]
+// RUN: test -e %t/test.o
+
+// 
=============================================================================
+// 2. Transformation-report write fails.
+// 
=============================================================================
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: %clang_cc1 -load %llvmshlibdir/SSAFTestTransformationPlugin%pluginext \
+// RUN:   -Wno-error=scalable-static-analysis-framework \
+// RUN:   --ssaf-source-transformation=test-transformation \
+// RUN:   --ssaf-global-scope-analysis-result=%S/Inputs/empty-suite.json \
+// RUN:   --ssaf-src-edit-file=%t/edits.yaml \
+// RUN:   --ssaf-transformation-report-file=%t/missing-dir/report.sarif \
+// RUN:   --ssaf-compilation-unit-id=cu \
+// RUN:   -emit-obj -o %t/test.o %s 2>&1 | FileCheck 
--check-prefix=REPORT-FAIL %s
+// REPORT-FAIL: warning: failed to write transformation report to 
'{{.*}}/missing-dir/report.sarif'{{.*}}[-Wscalable-static-analysis-framework]
+// RUN: test -e %t/test.o
+
+void foo() {}
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index 8dd0084c53224..6cb6877fed55c 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -211,7 +211,9 @@ endif()
 if(CLANG_PLUGIN_SUPPORT AND LLVM_ENABLE_PLUGINS AND NOT WIN32)
   list(APPEND CLANG_TEST_DEPS
     SSAFExamplePlugin
+    SSAFTestTransformationPlugin
     )
+  add_subdirectory(Analysis/Scalable/source-edit-generation/Plugins)
 endif()
 
 if (HAVE_CLANG_REPL_SUPPORT)

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to