[clang] [clang][ssaf] Integrate source-edit-generation (PR #208590)
Jan Korous via cfe-commits
cfe-commits at lists.llvm.org
Thu Jul 9 17:05:45 PDT 2026
https://github.com/jkorous-apple created https://github.com/llvm/llvm-project/pull/208590
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.
Assisted-By: Claude Opus 4.7
>From cfbf22e4351a66d877fcc01c74f92fe7146778d3 Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Thu, 9 Jul 2026 14:22:01 -0700
Subject: [PATCH] [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`.
Assisted-By: Claude Opus 4.7
---
.../user-docs/SourceEditGeneration.rst | 63 +++++
.../clang/Basic/DiagnosticFrontendKinds.td | 46 ++++
clang/include/clang/Basic/DiagnosticIDs.h | 2 +-
clang/include/clang/Frontend/SSAFOptions.h | 21 ++
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 | 234 ++++++++++++++++++
clang/test/Analysis/Scalable/help.cpp | 8 +
.../Inputs/empty-suite.json | 5 +
.../Inputs/two-function-suite.json | 26 ++
.../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, 811 insertions(+), 1 deletion(-)
create mode 100644 clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.rst
create mode 100644 clang/include/clang/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.h
create mode 100644 clang/lib/ScalableStaticAnalysis/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/ScalableStaticAnalysis/user-docs/SourceEditGeneration.rst b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.rst
new file mode 100644
index 0000000000000..25839c6f32c7f
--- /dev/null
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.rst
@@ -0,0 +1,63 @@
+==============================
+Source Edit Generation
+==============================
+
+Source edit generation relies on a ``WPASuite`` result produced by an
+earlier whole-program analysis. It 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 is shared
+with the summary extraction step. A given compilation unit needs to
+receive the same identifier for both summary extraction and source
+edit generation.
+
+.. 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 JSON
+ document; the file extension is not interpreted.
+ * - ``--ssaf-compilation-unit-id=<id>``
+ - Stable identifier for this translation unit (also required by
+ the summary extraction).
+
+When ``--ssaf-source-transformation=`` is non-empty the framework wraps
+the active ``FrontendAction`` in a ``SourceTransformationFrontendAction``;
+otherwise the compile is byte-for-byte unchanged.
+
+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 viewer.
diff --git a/clang/include/clang/Basic/DiagnosticFrontendKinds.td b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
index cef2fc32a1642..4677af8400e26 100644
--- a/clang/include/clang/Basic/DiagnosticFrontendKinds.td
+++ b/clang/include/clang/Basic/DiagnosticFrontendKinds.td
@@ -435,6 +435,52 @@ def warn_ssaf_tu_summary_requires_compilation_unit_id :
"'--ssaf-compilation-unit-id=' to be set">,
InGroup<ScalableStaticAnalysis>, DefaultError;
+def warn_ssaf_source_transformation_unknown_name :
+ Warning<"no source transformation registered with name: %0">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_wpa_file :
+ Warning<"option '--ssaf-source-transformation=' requires "
+ "'--ssaf-global-scope-analysis-result=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_edit_file :
+ Warning<"option '--ssaf-source-transformation=' requires "
+ "'--ssaf-src-edit-file=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_report_file :
+ Warning<"option '--ssaf-source-transformation=' requires "
+ "'--ssaf-transformation-report-file=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_source_transformation_requires_compilation_unit_id :
+ Warning<"option '--ssaf-source-transformation=' requires "
+ "'--ssaf-compilation-unit-id=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_read_wpa_suite_failed :
+ Warning<"failed to read whole-program analysis result from '%0': %1">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_src_edit_file_requires_transformation :
+ Warning<"option '--ssaf-src-edit-file=' requires "
+ "'--ssaf-source-transformation=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_write_src_edit_failed :
+ Warning<"failed to write source edits to '%0': %1">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_transformation_report_file_requires_transformation :
+ Warning<"option '--ssaf-transformation-report-file=' requires "
+ "'--ssaf-source-transformation=' to be set">,
+ InGroup<ScalableStaticAnalysis>, DefaultError;
+
+def warn_ssaf_write_transformation_report_failed :
+ Warning<"failed to write transformation report to '%0': %1">,
+ InGroup<ScalableStaticAnalysis>, 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/SSAFOptions.h b/clang/include/clang/Frontend/SSAFOptions.h
index 738262cc4a713..c042a3fff6019 100644
--- a/clang/include/clang/Frontend/SSAFOptions.h
+++ b/clang/include/clang/Frontend/SSAFOptions.h
@@ -31,6 +31,27 @@ class SSAFOptions {
/// Controlled by: --ssaf-compilation-unit-id
std::string CompilationUnitId;
+ /// Name of the SSAF source transformation to run. Exactly one transformation
+ /// per invocation; non-empty implies the source-transformation pipeline is
+ /// active.
+ /// Controlled by: --ssaf-source-transformation
+ std::string SourceTransformation;
+
+ /// Path of the WPASuite input consumed by the source transformation. The
+ /// extension selects which serialization format reads it.
+ /// Controlled by: --ssaf-global-scope-analysis-result
+ std::string GlobalScopeAnalysisResult;
+
+ /// Path of the source-edit output file produced by the source
+ /// transformation.
+ /// Controlled by: --ssaf-src-edit-file
+ std::string SrcEditFile;
+
+ /// Path of the transformation-report output file produced by the source
+ /// transformation.
+ /// Controlled by: --ssaf-transformation-report-file
+ std::string TransformationReportFile;
+
/// Show the list of available SSAF summary extractors and exit.
/// Controlled by: --ssaf-list-extractors
LLVM_PREFERRED_TYPE(bool)
diff --git a/clang/include/clang/Options/Options.td b/clang/include/clang/Options/Options.td
index 677aaf00b5083..52711492d3cb1 100644
--- a/clang/include/clang/Options/Options.td
+++ b/clang/include/clang/Options/Options.td
@@ -982,6 +982,44 @@ def _ssaf_compilation_unit_id :
"produced SSAF TU summary. Required when '--ssaf-tu-summary-file=' is "
"set.">,
MarshallingInfoString<SSAFOpts<"CompilationUnitId">>;
+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<SSAFOpts<"SourceTransformation">>;
+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<SSAFOpts<"GlobalScopeAnalysisResult">>;
+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<SSAFOpts<"SrcEditFile">>;
+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<SSAFOpts<"TransformationReportFile">>;
def Xarch__
: JoinedAndSeparate<["-"], "Xarch_">,
Flags<[NoXarchOption]>,
diff --git a/clang/include/clang/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.h b/clang/include/clang/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.h
new file mode 100644
index 0000000000000..1d554ca8343b8
--- /dev/null
+++ b/clang/include/clang/ScalableStaticAnalysis/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_SCALABLESTATICANALYSIS_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H
+#define LLVM_CLANG_SCALABLESTATICANALYSIS_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 SSAFOptions::GlobalScopeAnalysisResult 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_SCALABLESTATICANALYSIS_FRONTEND_SOURCETRANSFORMATIONFRONTENDACTION_H
diff --git a/clang/lib/Driver/ToolChains/Clang.cpp b/clang/lib/Driver/ToolChains/Clang.cpp
index a30b01a675b99..32c8103e3556a 100644
--- a/clang/lib/Driver/ToolChains/Clang.cpp
+++ b/clang/lib/Driver/ToolChains/Clang.cpp
@@ -8073,6 +8073,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 a206770c8490f..6c3a5d096c99b 100644
--- a/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
+++ b/clang/lib/FrontendTool/ExecuteCompilerInvocation.cpp
@@ -24,6 +24,7 @@
#include "clang/FrontendTool/Utils.h"
#include "clang/Options/Options.h"
#include "clang/Rewrite/Frontend/FrontendActions.h"
+#include "clang/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.h"
#include "clang/ScalableStaticAnalysis/Frontend/TUSummaryExtractorFrontendAction.h"
#include "clang/ScalableStaticAnalysis/SSAFForceLinker.h" // IWYU pragma: keep
#include "clang/StaticAnalyzer/Frontend/AnalyzerHelpFlags.h"
@@ -214,6 +215,12 @@ CreateFrontendAction(CompilerInstance &CI) {
Act = std::make_unique<ssaf::TUSummaryExtractorFrontendAction>(
std::move(Act));
}
+ if (!CI.getSSAFOpts().SourceTransformation.empty() ||
+ !CI.getSSAFOpts().SrcEditFile.empty() ||
+ !CI.getSSAFOpts().TransformationReportFile.empty()) {
+ Act = std::make_unique<ssaf::SourceTransformationFrontendAction>(
+ std::move(Act));
+ }
return Act;
}
diff --git a/clang/lib/ScalableStaticAnalysis/Frontend/CMakeLists.txt b/clang/lib/ScalableStaticAnalysis/Frontend/CMakeLists.txt
index 74e1d0cbe1ed0..58a4806754b45 100644
--- a/clang/lib/ScalableStaticAnalysis/Frontend/CMakeLists.txt
+++ b/clang/lib/ScalableStaticAnalysis/Frontend/CMakeLists.txt
@@ -4,6 +4,7 @@ set(LLVM_LINK_COMPONENTS
)
add_clang_library(clangScalableStaticAnalysisFrontend
+ SourceTransformationFrontendAction.cpp
TUSummaryExtractorFrontendAction.cpp
LINK_LIBS
@@ -12,5 +13,7 @@ add_clang_library(clangScalableStaticAnalysisFrontend
clangFrontend
clangScalableStaticAnalysisAnalyses
clangScalableStaticAnalysisCore
+ clangScalableStaticAnalysisSourceTransformation
clangSema
+ clangToolingCore
)
diff --git a/clang/lib/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.cpp b/clang/lib/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.cpp
new file mode 100644
index 0000000000000..1fdc82009f8cc
--- /dev/null
+++ b/clang/lib/ScalableStaticAnalysis/Frontend/SourceTransformationFrontendAction.cpp
@@ -0,0 +1,234 @@
+//===- 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/ScalableStaticAnalysis/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/Frontend/SSAFOptions.h"
+#include "clang/ScalableStaticAnalysis/Core/Serialization/SerializationFormat.h"
+#include "clang/ScalableStaticAnalysis/Core/Serialization/SerializationFormatRegistry.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/SARIFTransformationReportFormat.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/SourceEditEmitter.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/Transformation.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationRegistry.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/TransformationReportEmitter.h"
+#include "clang/ScalableStaticAnalysis/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 SSAFOptions &Opts,
+ StringRef InFile);
+
+ void HandleTranslationUnit(ASTContext &Ctx) override;
+
+ WPASuite Suite;
+ AccumulatorSourceEditEmitter Edits;
+ AccumulatorReportEmitter Report;
+ const SSAFOptions &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 SSAFOptions &Opts) {
+ bool Reported = false;
+
+ if (!Opts.SourceTransformation.empty()) {
+ if (Opts.GlobalScopeAnalysisResult.empty()) {
+ Diags.Report(diag::warn_ssaf_source_transformation_requires_wpa_file);
+ Reported = true;
+ }
+ if (Opts.SrcEditFile.empty()) {
+ Diags.Report(diag::warn_ssaf_source_transformation_requires_edit_file);
+ Reported = true;
+ }
+ if (Opts.TransformationReportFile.empty()) {
+ Diags.Report(diag::warn_ssaf_source_transformation_requires_report_file);
+ Reported = true;
+ }
+ if (Opts.CompilationUnitId.empty()) {
+ Diags.Report(
+ diag::warn_ssaf_source_transformation_requires_compilation_unit_id);
+ Reported = true;
+ }
+ } else {
+ if (!Opts.SrcEditFile.empty()) {
+ Diags.Report(diag::warn_ssaf_src_edit_file_requires_transformation);
+ Reported = true;
+ }
+ if (!Opts.TransformationReportFile.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 SSAFOptions &Opts = CI.getSSAFOpts();
+ DiagnosticsEngine &Diags = CI.getDiagnostics();
+
+ if (reportOrphanFlagMisuse(Diags, Opts))
+ return nullptr;
+ if (Opts.SourceTransformation.empty())
+ return nullptr;
+
+ if (!isTransformationRegistered(Opts.SourceTransformation)) {
+ Diags.Report(diag::warn_ssaf_source_transformation_unknown_name)
+ << Opts.SourceTransformation;
+ return nullptr;
+ }
+
+ std::optional<StringRef> WPAExt =
+ bareExtension(Opts.GlobalScopeAnalysisResult);
+ std::unique_ptr<SerializationFormat> WPAFormat =
+ WPAExt && isFormatRegistered(*WPAExt) ? makeFormat(*WPAExt) : nullptr;
+ if (!WPAFormat) {
+ Diags.Report(diag::warn_ssaf_read_wpa_suite_failed)
+ << Opts.GlobalScopeAnalysisResult << "unknown serialization format";
+ return nullptr;
+ }
+ llvm::sys::sandbox::ScopedSetting Guard = llvm::sys::sandbox::scopedDisable();
+ llvm::Expected<WPASuite> SuiteOrErr =
+ WPAFormat->readWPASuite(Opts.GlobalScopeAnalysisResult);
+ if (!SuiteOrErr) {
+ Diags.Report(diag::warn_ssaf_read_wpa_suite_failed)
+ << Opts.GlobalScopeAnalysisResult
+ << llvm::toString(SuiteOrErr.takeError());
+ return nullptr;
+ }
+
+ return std::unique_ptr<SourceTransformationRunner>{
+ new SourceTransformationRunner(std::move(*SuiteOrErr), Opts, InFile)};
+}
+
+SourceTransformationRunner::SourceTransformationRunner(WPASuite Suite,
+ const SSAFOptions &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.SourceTransformation, 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.SrcEditFile)) {
+ Ctx.getDiagnostics().Report(diag::warn_ssaf_write_src_edit_failed)
+ << Opts.SrcEditFile << llvm::toString(std::move(Err));
+ }
+
+ // And the transformation report.
+ ReportDocument ReportDoc{Opts.SourceTransformation, Ctx.getSourceManager(),
+ std::move(Report.Results)};
+ if (auto Err = writeSARIFTransformationReport(
+ ReportDoc, Opts.TransformationReportFile)) {
+ Ctx.getDiagnostics().Report(
+ diag::warn_ssaf_write_transformation_report_failed)
+ << Opts.TransformationReportFile << 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..c9a2c4421b807
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/empty-suite.json
@@ -0,0 +1,5 @@
+{
+ "id_table": [],
+ "results": [],
+ "type": "WPASuite"
+}
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..bb63780cce01d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/source-edit-generation/Inputs/two-function-suite.json
@@ -0,0 +1,26 @@
+{
+ "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": [],
+ "type": "WPASuite"
+}
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..7f170be16d2cc
--- /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:clangScalableStaticAnalysisCore,INTERFACE_INCLUDE_DIRECTORIES>
+ $<TARGET_PROPERTY:clangScalableStaticAnalysisSourceTransformation,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..d3edc34882cef
--- /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/ScalableStaticAnalysis/Core/Model/EntityIdTable.h"
+#include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/WPASuite.h"
+#include "clang/ScalableStaticAnalysis/SourceTransformation/Transformation.h"
+#include "clang/ScalableStaticAnalysis/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 15170bbc2d1ed..92883005dd88d 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