[clang] [clang][ssaf] Implement single TU-scoped source transformation (PR #203660)
Jan Korous via cfe-commits
cfe-commits at lists.llvm.org
Fri Jun 12 19:21:39 PDT 2026
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 <jkorous at apple.com>
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 <jkorous at apple.com>
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 <jkorous at apple.com>
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 <jkorous at apple.com>
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 at foo#"
+ }
+ },
+ {
+ "id": 1,
+ "name": {
+ "namespace": [
+ { "kind": "LinkUnit", "name": "test.exe" }
+ ],
+ "suffix": "",
+ "usr": "c:@F at 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)
More information about the cfe-commits
mailing list