[llvm-branch-commits] [clang] [clang][ssaf][orchestrator] Merge transformation reports per link unit (PR #232548)

Jan Korous via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu Oct 15 16:55:34 PDT 2026


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

In the transformation pass, the ar and ld shims now also merge the
transformation reports of what they consume, with
clang-ssaf-report-merge, and their conflict reports, with
clang-ssaf-conflict-merge, adding the conflicts of their own edit merge.
The link unit's reports therefore cover every translation unit and every
merge behind it, and the run names them on stdout in place of the
conflict files of the individual merges.

A compilation whose transformation fails leaves no report behind, as it
leaves no edits.

This relands ba6d822f43f0 (#231957), reverted with its stack in
782610a701b4 (#232274).

Assisted-by: Claude



>From 5cbd40f9650c209beca7e19b5f0286511f25877c Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Thu, 15 Oct 2026 14:30:57 -0700
Subject: [PATCH] [clang][ssaf][orchestrator] Merge transformation reports per
 link unit

In the transformation pass, the ar and ld shims now also merge the
transformation reports of what they consume, with
clang-ssaf-report-merge, and their conflict reports, with
clang-ssaf-conflict-merge, adding the conflicts of their own edit merge.
The link unit's reports therefore cover every translation unit and every
merge behind it, and the run names them on stdout in place of the
conflict files of the individual merges.

A compilation whose transformation fails leaves no report behind, as it
leaves no edits.

This relands ba6d822f43f0 (#231957), reverted with its stack in
782610a701b4 (#232274).

Assisted-by: Claude
---
 .../developer-docs/MakeOrchestratorShims.md   |  7 +-
 .../user-docs/MakeProjects.md                 | 51 +++++++++--
 .../Scalable/ssaf-orchestrator-make/help.test |  2 +
 .../transformation-reports.test               | 88 ++++++++++++++++++
 .../ssaf-orchestrator-make/two-pass.test      |  3 +-
 .../clang_ssaf_orchestrator_make/config.py    |  4 +
 .../config_orchestrator.py                    | 24 +++++
 .../failure_logs.py                           |  2 +
 .../logging_diagnostics.py                    |  8 +-
 .../clang_ssaf_orchestrator_make/options.py   | 18 ++++
 .../orchestrator.py                           |  7 +-
 .../clang_ssaf_orchestrator_make/shims/ar.py  |  4 +-
 .../shims/clang.py                            | 29 +++---
 .../shims/commands.py                         |  7 +-
 .../clang_ssaf_orchestrator_make/shims/ld.py  |  4 +-
 .../shims/shim.py                             | 89 +++++++++++++++----
 .../ssaf_artifact_repository.py               | 14 ++-
 .../tests/configs.py                          |  2 +
 .../tests/test_ar_shim.py                     |  5 +-
 .../tests/test_clang_shim.py                  | 37 ++++++--
 .../tests/test_config.py                      | 10 ++-
 .../tests/test_config_orchestrator.py         | 12 +++
 .../tests/test_ld_shim.py                     | 83 ++++++++++++++++-
 .../tests/test_logging_diagnostics.py         | 11 +--
 .../tests/test_ssaf_artifact_repository.py    | 34 +++----
 25 files changed, 462 insertions(+), 93 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/transformation-reports.test

diff --git a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
index 0d6d3c10ef2e0..375e3eda16cbe 100644
--- a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
+++ b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
@@ -48,7 +48,7 @@ The shim bodies live in the package, as `clang_ssaf_orchestrator_make.shims`,
 split by what they do:
 
 ```text
-shim.py          the Shim class, its diagnostics, merge_edits
+shim.py          the Shim class, its diagnostics, the transformation merges
 commands.py      Files, is_object, is_archive, artifact_of, summary_of,
                  filter_build_artifacts, unique
 driver_jobs.py   Job and the -### listing reader
@@ -131,6 +131,11 @@ transform    source-edit generation for a compilation
 link         clang-ssaf-linker over a link's inputs
 archive      the same for an archive
 merge        clang-ssaf-src-edit-merge over a link's or an archive's inputs
+transformation-report
+             clang-ssaf-report-merge over the same inputs
+conflict-report
+             clang-ssaf-conflict-merge over the same inputs and the
+             conflicts of the merge itself
 forward      handing a job to the shim that stands in for its tool
 missing      an artifact with nothing analysed behind it
 driver-jobs  the driver refused to list a command's jobs
diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
index 648da537ae1d7..ccc431cdf674f 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
@@ -45,8 +45,9 @@ There are three workflows, each going one step further than the last:
 - **Whole-program analysis**, adding `--analysis`. `clang-ssaf-analyzer` runs
   the named analyses over the link unit summary.
 - **Whole-program transformation**, adding `--transformation`. A second build
-  generates source edits from the analysis result, and they are merged across
-  the whole program.
+  generates source edits and transformation reports from the analysis result,
+  and they are merged across the whole program, together with the reports of
+  the edits that conflicted.
 
 Summary extraction and linking:
 
@@ -67,7 +68,7 @@ $ clang-ssaf-orchestrator-make \
     -- make -j8
 ```
 
-Whole-program transformation, which ends in merged source edits:
+Whole-program transformation, which ends in merged source edits and reports:
 
 ```console
 $ clang-ssaf-orchestrator-make \
@@ -111,6 +112,12 @@ orchestrator only the `make` invocation.
    * - ``--ssaf-src-edit-merge=<path>``
      - Path to ``clang-ssaf-src-edit-merge``. Defaults to the name on
        ``PATH``, and is only required with ``--transformation``.
+   * - ``--ssaf-report-merge=<path>``
+     - Path to ``clang-ssaf-report-merge``. Defaults to the name on
+       ``PATH``, and is only required with ``--transformation``.
+   * - ``--ssaf-conflict-merge=<path>``
+     - Path to ``clang-ssaf-conflict-merge``. Defaults to the name on
+       ``PATH``, and is only required with ``--transformation``.
    * - ``--ssaf-extra-clang-arg=<arg>``
      - Extra argument for every SSAF compilation. Repeatable.
    * - ``-k``, ``--keep-going``
@@ -160,9 +167,14 @@ translation units, and only the whole path tells them apart.
    * - ``<output-dir>/ssaf-artifacts/<path>.edits.yaml``
      - Source edits for the ``<path>`` build artifact.
    * - ``<output-dir>/ssaf-artifacts/<path>.conflicts.sarif``
-     - Edits dropped because they conflicted.
+     - Edits dropped because they conflicted when the build assembled
+       ``<path>``.
+   * - ``<output-dir>/ssaf-artifacts/<path>.conflict-report.sarif``
+     - Every conflict behind the ``<path>`` build artifact: its own, and its
+       inputs'.
    * - ``<output-dir>/ssaf-artifacts/<path>.report.sarif``
-     - Transformation report for the ``<path>`` build artifact.
+     - Transformation report for the ``<path>`` build artifact: a
+       compilation's own, and its inputs' merged where the build assembled it.
    * - ``<output-dir>/config-analysis.json``
      - Configuration for the analysis pass, the first build pass.
    * - ``<output-dir>/config-transformation.json``
@@ -187,6 +199,35 @@ input contributes the summary filed under its own id or nothing at all,
 so an artifact whose SSAF step failed takes the artifacts behind it down
 with it, and the run says so and exits non-zero.
 
+## Results
+
+A run names what it produced on stdout, which it shares with the build, one
+`key: value` line each:
+
+```{eval-rst}
+.. list-table::
+   :header-rows: 1
+
+   * - Key
+     - Value
+   * - ``summaries``
+     - How many summaries were written, for compilations, archives and links.
+   * - ``link-unit``
+     - The top-most build artifact.
+   * - ``link-unit-summary``
+     - Its summary.
+   * - ``analysis-result``
+     - The whole-program analysis result, with ``--analysis``.
+   * - ``source-edits``
+     - The link unit's merged source edits, with ``--transformation``.
+   * - ``transformation-report``
+     - The link unit's merged transformation report.
+   * - ``conflict-report``
+     - Every conflict found while merging the link unit's source edits.
+   * - ``output-dir``
+     - The output directory.
+```
+
 ## Failure handling
 
 A failing build step fails the run with the build's own exit status, and no
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
index 3fefaae801bce..387af637d8dd8 100644
--- a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
@@ -13,6 +13,8 @@
 # SSAF: --ssaf-linker
 # SSAF: --ssaf-analyzer
 # SSAF: --ssaf-src-edit-merge
+# SSAF: --ssaf-report-merge
+# SSAF: --ssaf-conflict-merge
 # SSAF: --ssaf-extra-clang-arg
 # SSAF: --keep-going
 # SSAF: --verbose
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/transformation-reports.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/transformation-reports.test
new file mode 100644
index 0000000000000..5cb1fa5d55d75
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/transformation-reports.test
@@ -0,0 +1,88 @@
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# The link unit's transformation report holds the findings of every
+# translation unit once, and its conflict report holds the conflicts found
+# while merging any of its parts.
+
+# RUN: cd %t
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow,UnsafeBufferUsage \
+# RUN:     --analysis=UnsafeBufferReachableAnalysisResult \
+# RUN:     --transformation=cpp-bounded-buffers \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make \
+# RUN:   | FileCheck %s --check-prefix=STDOUT
+# STDOUT: source-edits: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.edits.yaml
+# STDOUT: transformation-report: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.report.sarif
+# STDOUT: conflict-report: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.conflict-report.sarif
+
+# Every translation unit reports the declaration group in the header; the
+# link unit's report has each of its two declarators once.
+# RUN: FileCheck %s --check-prefix=REPORT \
+# RUN:   --input-file=%t/out/ssaf-artifacts%t/build/prg.report.sarif
+# RUN: grep 'multi-declarator group' \
+# RUN:   %t/out/ssaf-artifacts%t/build/prg.report.sarif | count 4
+# REPORT:      "results": [
+# REPORT:        "uri": "file://{{.*}}/src/main.cpp"
+# REPORT:        "uri": "file://{{.*}}/src/main.cpp"
+# REPORT:        "uri": "file://{{.*}}/src/shared.h"
+# REPORT:        "text": "declarator of a multi-declarator group is not yet rewritten"
+# REPORT:        "uri": "file://{{.*}}/src/shared.h"
+# REPORT:        "text": "declarator of a multi-declarator group is not yet rewritten"
+# REPORT-NOT:    "uri":
+# REPORT:      "tool": {
+
+# The two halves of the archive disagree on the header, so the archive's
+# merge drops both edits. The link's own merge finds no conflict, yet the
+# link unit's conflict report has the archive's.
+# RUN: grep clang-reforge-replacement-conflict \
+# RUN:   %t/out/ssaf-artifacts%t/build/libparts.a.conflicts.sarif | count 1
+# RUN: not grep clang-reforge-replacement-conflict \
+# RUN:   %t/out/ssaf-artifacts%t/build/prg.conflicts.sarif
+# RUN: FileCheck %s --check-prefix=CONFLICTS \
+# RUN:   --input-file=%t/out/ssaf-artifacts%t/build/prg.conflict-report.sarif
+# CONFLICTS:      "results": [
+# CONFLICTS:        "uri": "file://{{.*}}/src/shared.h"
+# CONFLICTS:        "byteOffset": 39
+# CONFLICTS:        "ruleId": "clang-reforge-replacement-conflict"
+# CONFLICTS-NOT:    "ruleId":
+# CONFLICTS:        "name": "clang-ssaf-src-edit-merge",
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/%.o: src/%.cpp src/shared.h
+	$(CXX) -c -o $@ $<
+build/libparts.a: build/a.o build/b.o
+	ar rcs $@ build/a.o build/b.o
+build/prg: build/main.o build/libparts.a
+	$(CXX) -o $@ build/main.o build/libparts.a
+
+#--- src/shared.h
+extern int *p, *q;
+extern
+#if USE_LONG
+long
+#else
+int
+#endif
+*g;
+
+#--- src/a.cpp
+#include "shared.h"
+int *g;
+int a() { return p[1] + q[1] + g[5]; }
+
+#--- src/b.cpp
+#define USE_LONG 1
+#include "shared.h"
+long b() { return p[2] + g[7]; }
+
+#--- src/main.cpp
+#include "shared.h"
+int *p, *q;
+int a();
+long b();
+int main() { return a() + (int)b(); }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test
index c9b19bd5ac7b2..6d5362981b369 100644
--- a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test
@@ -16,7 +16,8 @@
 # STDOUT: link-unit: {{[^ ]*}}/build/prg
 # STDOUT: analysis-result: {{[^ ]*}}/out/wpa.json
 # STDOUT: source-edits: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.edits.yaml
-# STDOUT-NOT: edit-conflicts
+# STDOUT: transformation-report: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.report.sarif
+# STDOUT: conflict-report: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.conflict-report.sarif
 
 # RUN: ls %t/out/ssaf-artifacts%t/build/main.o.summary.json %t/out/ssaf-artifacts%t/build/parse.o.summary.json
 # RUN: ls %t/out/ssaf-artifacts%t/build/main.o.edits.yaml \
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
index bceca7b4907cb..4805777788bf2 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
@@ -96,11 +96,15 @@ def __init__(
         ssaf_clang=None,
         ssaf_extra_clang_args=(),
         ssaf_src_edit_merge=None,
+        ssaf_report_merge=None,
+        ssaf_conflict_merge=None,
         transformation=None,
         link_unit_id=None,
     ):
         super().__init__(ssaf_clang, ssaf_extra_clang_args)
         self.ssaf_src_edit_merge = ssaf_src_edit_merge
+        self.ssaf_report_merge = ssaf_report_merge
+        self.ssaf_conflict_merge = ssaf_conflict_merge
         self.transformation = transformation
         self.link_unit_id = link_unit_id
 
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
index f47b4f05cc73e..8e15e9a198aee 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
@@ -127,6 +127,28 @@ def configure(command_line_opts, env):
         if command_line_opts.transformation
         else None
     )
+    ssaf_report_merge = (
+        _resolve_ssaf_tool(
+            "--ssaf-report-merge",
+            "clang-ssaf-report-merge",
+            command_line_opts.ssaf_report_merge,
+            installed_shim_dir_path,
+            errors,
+        )
+        if command_line_opts.transformation
+        else None
+    )
+    ssaf_conflict_merge = (
+        _resolve_ssaf_tool(
+            "--ssaf-conflict-merge",
+            "clang-ssaf-conflict-merge",
+            command_line_opts.ssaf_conflict_merge,
+            installed_shim_dir_path,
+            errors,
+        )
+        if command_line_opts.transformation
+        else None
+    )
 
     if errors:
         return None, options.Problem(
@@ -156,6 +178,8 @@ def configure(command_line_opts, env):
     transformation_config = (
         config.SSAFTransformationPassConfig(
             ssaf_src_edit_merge=ssaf_src_edit_merge,
+            ssaf_report_merge=ssaf_report_merge,
+            ssaf_conflict_merge=ssaf_conflict_merge,
             transformation=command_line_opts.transformation,
             **common_pass_config,
         )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
index 80ca84d7f52a9..5758619a95b6c 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
@@ -18,6 +18,8 @@ class SSAFFailureKind(enum.Enum):
     LINK = "link"
     ARCHIVE = "archive"
     MERGE = "merge"
+    TRANSFORMATION_REPORT = "transformation-report"
+    CONFLICT_REPORT = "conflict-report"
     FORWARD = "forward"
     MISSING = "missing"
     DRIVER_JOBS = "driver-jobs"
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
index 96c6ff3293b63..a539bc46ca604 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
@@ -41,7 +41,9 @@ def report_analysis_result(wpa_result_path):
     report_info("analysis-result", wpa_result_path)
 
 
-def report_transformation(merged, conflicts_paths):
+def report_transformation(merged, transformation_report, conflict_report):
     report_info("source-edits", merged)
-    for conflicts_path in conflicts_paths:
-        report_info("edit-conflicts", conflicts_path)
+    if transformation_report:
+        report_info("transformation-report", transformation_report)
+    if conflict_report:
+        report_info("conflict-report", conflict_report)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
index 3e720de577bf3..e5a1decc3b597 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
@@ -79,6 +79,18 @@ def build_parser():
         default=None,
         help="Path to clang-ssaf-src-edit-merge (default: found on PATH).",
     )
+    parser.add_argument(
+        "--ssaf-report-merge",
+        metavar="<path>",
+        default=None,
+        help="Path to clang-ssaf-report-merge (default: found on PATH).",
+    )
+    parser.add_argument(
+        "--ssaf-conflict-merge",
+        metavar="<path>",
+        default=None,
+        help="Path to clang-ssaf-conflict-merge (default: found on PATH).",
+    )
     parser.add_argument(
         "--ssaf-extra-clang-arg",
         metavar="<arg>",
@@ -121,6 +133,8 @@ def __init__(
         ssaf_linker=None,
         ssaf_analyzer=None,
         ssaf_src_edit_merge=None,
+        ssaf_report_merge=None,
+        ssaf_conflict_merge=None,
         keep_going=False,
         verbose=False,
     ):
@@ -134,6 +148,8 @@ def __init__(
         self.ssaf_linker = ssaf_linker
         self.ssaf_analyzer = ssaf_analyzer
         self.ssaf_src_edit_merge = ssaf_src_edit_merge
+        self.ssaf_report_merge = ssaf_report_merge
+        self.ssaf_conflict_merge = ssaf_conflict_merge
         self.keep_going = keep_going
         self.verbose = verbose
 
@@ -166,6 +182,8 @@ def resolve(argv):
             ssaf_linker=args.ssaf_linker,
             ssaf_analyzer=args.ssaf_analyzer,
             ssaf_src_edit_merge=args.ssaf_src_edit_merge,
+            ssaf_report_merge=args.ssaf_report_merge,
+            ssaf_conflict_merge=args.ssaf_conflict_merge,
             keep_going=args.keep_going,
             verbose=args.verbose,
         ),
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
index ce2a73cbebb0b..06ba03e57fe49 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
@@ -195,7 +195,12 @@ def run_ssaf_transformation_pass(
         )
     logging_diagnostics.report_transformation(
         ssaf_artifact_repo.get_path_for_artifact(edits),
-        ssaf_artifact_repo.get_conflicts(),
+        ssaf_artifact_repo.get_path_if_present(
+            get_ssaf_artifact_id(link_unit, SSAFArtifactKind.REPORT)
+        ),
+        ssaf_artifact_repo.get_path_if_present(
+            get_ssaf_artifact_id(link_unit, SSAFArtifactKind.CONFLICT_REPORT)
+        ),
     )
     return None
 
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
index 70237e0e6f888..e8c7268f68ea8 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
@@ -17,7 +17,7 @@
     summary_of,
     unique,
 )
-from .shim import merge_edits
+from .shim import merge_transformation_outputs
 
 
 _CREATING_OPERATIONS = frozenset("rqm")
@@ -63,7 +63,7 @@ def invoke_ssaf_tools(shim, cwd):
     )
 
     if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
-        merge_edits(shim, build_artifact_id, input_build_artifact_ids)
+        merge_transformation_outputs(shim, build_artifact_id, input_build_artifact_ids)
         return
 
     shim.build_graph.record_build_step(
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
index f00ae9bf62767..87ddf85e93994 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
@@ -90,7 +90,7 @@ def _summary_extraction_args(shim, build_artifact_id):
         "--ssaf-compilation-unit-id=" + build_artifact_id.get_path(),
         "--ssaf-tu-summary-file=" + summary,
     ]
-    return summary, extra + shim.pass_config.ssaf_extra_clang_args
+    return [summary], extra + shim.pass_config.ssaf_extra_clang_args
 
 
 def _transformation_args(shim, build_artifact_id):
@@ -100,6 +100,11 @@ def _transformation_args(shim, build_artifact_id):
             get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.EDITS)
         )
     )
+    report = utils.make_parents(
+        ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.REPORT)
+        )
+    )
     extra = [
         "--ssaf-source-transformation=" + shim.pass_config.transformation,
         "--ssaf-global-scope-analysis-result="
@@ -107,14 +112,9 @@ def _transformation_args(shim, build_artifact_id):
         "--ssaf-compilation-unit-id=" + build_artifact_id.get_path(),
         "--ssaf-link-unit-id=" + shim.pass_config.link_unit_id,
         "--ssaf-src-edit-file=" + edits,
-        "--ssaf-transformation-report-file="
-        + utils.make_parents(
-            ssaf_artifact_repo.get_path_for_artifact(
-                get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.REPORT)
-            )
-        ),
+        "--ssaf-transformation-report-file=" + report,
     ]
-    return edits, extra + shim.pass_config.ssaf_extra_clang_args
+    return [edits, report], extra + shim.pass_config.ssaf_extra_clang_args
 
 
 def _run_clang_driven_tool(shim, cwd):
@@ -129,18 +129,19 @@ def _run_clang_driven_tool(shim, cwd):
     output = os.path.join(cwd, job.output)
     build_artifact_id = BuildArtifactId(output)
     if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
-        artifact, extra = _transformation_args(shim, build_artifact_id)
+        artifacts, extra = _transformation_args(shim, build_artifact_id)
         failure_kind = SSAFFailureKind.TRANSFORM
     else:
-        artifact, extra = _summary_extraction_args(shim, build_artifact_id)
+        artifacts, extra = _summary_extraction_args(shim, build_artifact_id)
         failure_kind = SSAFFailureKind.EXTRACT
     ssaf_command = [shim.pass_config.ssaf_clang] + shim.argv[1:] + extra
 
     if not shim.run(ssaf_command, failure_kind, build_artifact_id):
-        try:
-            os.remove(artifact)
-        except OSError:
-            pass
+        for artifact in artifacts:
+            try:
+                os.remove(artifact)
+            except OSError:
+                pass
         logging_diagnostics.report_error(
             "discarded incomplete artifact for %s" % build_artifact_id.get_path()
         )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
index f290ae9097797..e604a13c5ffaa 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
@@ -23,10 +23,9 @@ def is_archive(filename):
 
 
 def artifact_of(ssaf_artifact_repo, build_artifact_id, ssaf_artifact_kind):
-    ssaf_artifact_id = get_ssaf_artifact_id(build_artifact_id, ssaf_artifact_kind)
-    if not ssaf_artifact_repo.has_artifact(ssaf_artifact_id):
-        return None
-    return ssaf_artifact_repo.get_path_for_artifact(ssaf_artifact_id)
+    return ssaf_artifact_repo.get_path_if_present(
+        get_ssaf_artifact_id(build_artifact_id, ssaf_artifact_kind)
+    )
 
 
 def summary_of(ssaf_artifact_repo, build_artifact_id):
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
index 3d1e5924623ed..d5df06388da4c 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
@@ -17,7 +17,7 @@
     summary_of,
     unique,
 )
-from .shim import merge_edits
+from .shim import merge_transformation_outputs
 
 
 def identify_inputs_output(argv):
@@ -80,7 +80,7 @@ def invoke_ssaf_tools(shim, cwd):
     )
 
     if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
-        merge_edits(shim, build_artifact_id, input_build_artifact_ids)
+        merge_transformation_outputs(shim, build_artifact_id, input_build_artifact_ids)
         return
 
     shim.build_graph.record_build_step(
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
index 5180bced1ab30..fa5c8ddfba26b 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
@@ -188,28 +188,42 @@ def protect(self, work):
         return 0
 
 
-def merge_edits(shim, build_artifact_id, input_build_artifact_ids):
-    ssaf_artifact_repo = shim.ssaf_artifact_repo
+def _input_artifacts(ssaf_artifact_repo, input_build_artifact_ids, ssaf_artifact_kind):
     found = []
     for each in input_build_artifact_ids:
-        edits = artifact_of(ssaf_artifact_repo, each, SSAFArtifactKind.EDITS)
-        if edits:
-            found.append(edits)
-    found = unique(found)
-    if not found:
-        logging_diagnostics.report_note(
-            shim.workflow_config.verbose,
-            "no edits feed %s, nothing to merge" % build_artifact_id.get_path(),
+        artifact = artifact_of(ssaf_artifact_repo, each, ssaf_artifact_kind)
+        if artifact:
+            found.append(artifact)
+    return unique(found)
+
+
+def _output_artifact(ssaf_artifact_repo, build_artifact_id, ssaf_artifact_kind):
+    return utils.make_parents(
+        ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, ssaf_artifact_kind)
         )
+    )
+
+
+def _report_nothing_to_merge(shim, what, build_artifact_id):
+    logging_diagnostics.report_note(
+        shim.workflow_config.verbose,
+        "no %s feed %s, nothing to merge" % (what, build_artifact_id.get_path()),
+    )
+
+
+def merge_edits(shim, build_artifact_id, input_build_artifact_ids):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    found = _input_artifacts(
+        ssaf_artifact_repo, input_build_artifact_ids, SSAFArtifactKind.EDITS
+    )
+    if not found:
+        _report_nothing_to_merge(shim, "edits", build_artifact_id)
         return
     command = [shim.pass_config.ssaf_src_edit_merge] + found
     command += [
         "-o",
-        utils.make_parents(
-            ssaf_artifact_repo.get_path_for_artifact(
-                get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.EDITS)
-            )
-        ),
+        _output_artifact(ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.EDITS),
     ]
     command += [
         "--sarif-conflicts-out="
@@ -218,3 +232,48 @@ def merge_edits(shim, build_artifact_id, input_build_artifact_ids):
         )
     ]
     shim.run(command, SSAFFailureKind.MERGE, build_artifact_id)
+
+
+def merge_transformation_reports(shim, build_artifact_id, input_build_artifact_ids):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    found = _input_artifacts(
+        ssaf_artifact_repo, input_build_artifact_ids, SSAFArtifactKind.REPORT
+    )
+    if not found:
+        _report_nothing_to_merge(shim, "transformation reports", build_artifact_id)
+        return
+    command = [shim.pass_config.ssaf_report_merge] + found
+    command += [
+        "-o",
+        _output_artifact(
+            ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.REPORT
+        ),
+    ]
+    shim.run(command, SSAFFailureKind.TRANSFORMATION_REPORT, build_artifact_id)
+
+
+def merge_conflict_reports(shim, build_artifact_id, input_build_artifact_ids):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    found = _input_artifacts(
+        ssaf_artifact_repo, input_build_artifact_ids, SSAFArtifactKind.CONFLICT_REPORT
+    )
+    own = artifact_of(ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.CONFLICTS)
+    if own:
+        found.append(own)
+    if not found:
+        _report_nothing_to_merge(shim, "conflict reports", build_artifact_id)
+        return
+    command = [shim.pass_config.ssaf_conflict_merge] + found
+    command += [
+        "-o",
+        _output_artifact(
+            ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.CONFLICT_REPORT
+        ),
+    ]
+    shim.run(command, SSAFFailureKind.CONFLICT_REPORT, build_artifact_id)
+
+
+def merge_transformation_outputs(shim, build_artifact_id, input_build_artifact_ids):
+    merge_edits(shim, build_artifact_id, input_build_artifact_ids)
+    merge_transformation_reports(shim, build_artifact_id, input_build_artifact_ids)
+    merge_conflict_reports(shim, build_artifact_id, input_build_artifact_ids)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
index 5d920673350cf..51ced735233dd 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
@@ -14,6 +14,7 @@ class SSAFArtifactKind(enum.Enum):
     EDITS = ".edits.yaml"
     CONFLICTS = ".conflicts.sarif"
     REPORT = ".report.sarif"
+    CONFLICT_REPORT = ".conflict-report.sarif"
 
 
 class SSAFArtifactId:
@@ -54,6 +55,11 @@ def get_path_for_artifact(self, ssaf_artifact_id):
     def has_artifact(self, ssaf_artifact_id):
         return os.path.exists(self.get_path_for_artifact(ssaf_artifact_id))
 
+    def get_path_if_present(self, ssaf_artifact_id):
+        if not self.has_artifact(ssaf_artifact_id):
+            return None
+        return self.get_path_for_artifact(ssaf_artifact_id)
+
     def _all_paths_of_kind(self, ssaf_artifact_kind):
         return find_with_suffix(self.ssaf_artifact_dir, ssaf_artifact_kind.value)
 
@@ -68,11 +74,3 @@ def get_link_unit_name(self, summary_path):
 
     def count_summaries(self):
         return len(self._all_paths_of_kind(SSAFArtifactKind.SUMMARY))
-
-    def get_conflicts(self):
-        found = []
-        for path in self._all_paths_of_kind(SSAFArtifactKind.CONFLICTS):
-            document = read_json(path)
-            if document and any(run.get("results") for run in document.get("runs", [])):
-                found.append(path)
-        return sorted(found)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py b/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
index f95799c68e648..6e4da129726ac 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
@@ -26,6 +26,8 @@ def transformation_config(**overrides):
         ssaf_clang="/bin/clang",
         ssaf_extra_clang_args=[],
         ssaf_src_edit_merge="/bin/clang-ssaf-src-edit-merge",
+        ssaf_report_merge="/bin/clang-ssaf-report-merge",
+        ssaf_conflict_merge="/bin/clang-ssaf-conflict-merge",
         transformation="x",
     )
     fields.update(overrides)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py
index cfe3daaceabf3..8c30a7c30b939 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py
@@ -105,8 +105,9 @@ def test_in_the_transformation_pass_the_edits_are_merged_instead(self):
             ar.invoke_ssaf_tools(shim, self.root)
         self.assertEqual(
             [
-                "no edits feed %s, nothing to merge"
-                % self._id("build/libx.a").get_path()
+                "no %s feed %s, nothing to merge"
+                % (what, self._id("build/libx.a").get_path())
+                for what in ("edits", "transformation reports", "conflict reports")
             ],
             notes,
         )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
index decd46be3dd04..9ac58b9c75f96 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
@@ -156,7 +156,7 @@ def _shim(self, pass_config=None):
 
     def test_extraction_names_the_unit_and_where_its_summary_goes(self):
         shim = self._shim()
-        summary, extra = clang._summary_extraction_args(shim, self._id("build/a.o"))
+        (summary,), extra = clang._summary_extraction_args(shim, self._id("build/a.o"))
         self.assertIn("--ssaf-extract-summaries=PointerFlow", extra)
         self.assertIn(
             "--ssaf-compilation-unit-id=" + self._id("build/a.o").get_path(), extra
@@ -168,15 +168,13 @@ def test_transformation_names_the_link_unit_and_the_analysis_result(self):
         pass_config = configs.transformation_config()
         pass_config.name_link_unit(self._id("build/prg").get_path())
         shim = self._shim(pass_config)
-        edits, extra = clang._transformation_args(shim, self._id("build/a.o"))
+        (edits, report), extra = clang._transformation_args(shim, self._id("build/a.o"))
         self.assertIn("--ssaf-link-unit-id=" + self._id("build/prg").get_path(), extra)
         self.assertIn("--ssaf-src-edit-file=" + edits, extra)
         self.assertTrue(
             any(a.startswith("--ssaf-global-scope-analysis-result=") for a in extra)
         )
-        self.assertTrue(
-            any(a.startswith("--ssaf-transformation-report-file=") for a in extra)
-        )
+        self.assertIn("--ssaf-transformation-report-file=" + report, extra)
 
     def test_the_extra_clang_arguments_come_last(self):
         shim = self._shim(configs.analysis_config(ssaf_extra_clang_args=["-DX"]))
@@ -227,6 +225,35 @@ def test_an_incomplete_artifact_is_removed_and_reported(self):
         )
         self.assertFalse(os.path.exists(summary))
 
+    def test_a_failed_transformation_leaves_neither_edits_nor_report(self):
+        shim = configs.RecordingShim(
+            ["clang", "-cc1", "-o", "build/a.o", "a.cpp"],
+            self.out,
+            configs.transformation_config(link_unit_id="build/prg"),
+        )
+        records = configs.repository_for(self.out)
+        artifacts = [
+            records.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(self._id("build/a.o"), kind)
+            )
+            for kind in (
+                repository.SSAFArtifactKind.EDITS,
+                repository.SSAFArtifactKind.REPORT,
+            )
+        ]
+
+        def run(*args):
+            for artifact in artifacts:
+                open(artifact, "w").close()
+            return False
+
+        shim._shim.run = run
+        with mock.patch("sys.stderr", mock.MagicMock()):
+            clang._run_clang_driven_tool(shim, self.tmp)
+        self.assertEqual(
+            [False, False], [os.path.exists(artifact) for artifact in artifacts]
+        )
+
     def test_the_marker_chooses_the_frontend_half(self):
         shim = self._shim(
             [
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
index c6dbc9cd6341b..642a2a8c44a9c 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
@@ -39,6 +39,8 @@ def test_a_pass_carries_only_the_tools_it_runs(self):
                 "ssaf_clang",
                 "ssaf_extra_clang_args",
                 "ssaf_src_edit_merge",
+                "ssaf_report_merge",
+                "ssaf_conflict_merge",
                 "transformation",
                 "link_unit_id",
             },
@@ -120,7 +122,13 @@ def test_each_pass_file_carries_only_that_pass(self):
             {"extractors", "ssaf_linker", "analyses"}, set(first) - set(second)
         )
         self.assertEqual(
-            {"ssaf_src_edit_merge", "transformation", "link_unit_id"},
+            {
+                "ssaf_src_edit_merge",
+                "ssaf_report_merge",
+                "ssaf_conflict_merge",
+                "transformation",
+                "link_unit_id",
+            },
             set(second) - set(first),
         )
         self.assertEqual(
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
index 3ed1e13e2b154..6b7789601afe9 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
@@ -115,6 +115,8 @@ def setUp(self):
         self.linker = self._executable("clang-ssaf-linker")
         self.analyzer = self._executable("clang-ssaf-analyzer")
         self.merge = self._executable("clang-ssaf-src-edit-merge")
+        self.report_merge = self._executable("clang-ssaf-report-merge")
+        self.conflict_merge = self._executable("clang-ssaf-conflict-merge")
         self.project = os.path.join(self.tmp, "project")
         os.makedirs(self.project)
         self.addCleanup(os.chdir, os.getcwd())
@@ -158,6 +160,8 @@ def test_the_configuration_holds_the_resolved_tool_not_what_was_typed(self):
             "--ssaf-linker=clang-ssaf-linker",
             "--ssaf-analyzer=clang-ssaf-analyzer",
             "--ssaf-src-edit-merge=clang-ssaf-src-edit-merge",
+            "--ssaf-report-merge=clang-ssaf-report-merge",
+            "--ssaf-conflict-merge=clang-ssaf-conflict-merge",
             "--analysis=a",
             "--transformation=x",
             "--",
@@ -175,6 +179,8 @@ def test_the_configuration_holds_the_resolved_tool_not_what_was_typed(self):
         self.assertEqual(self.linker, analysis_config.ssaf_linker)
         self.assertEqual(self.analyzer, workflow_config.ssaf_analyzer)
         self.assertEqual(self.merge, transformation_config.ssaf_src_edit_merge)
+        self.assertEqual(self.report_merge, transformation_config.ssaf_report_merge)
+        self.assertEqual(self.conflict_merge, transformation_config.ssaf_conflict_merge)
 
     def test_a_run_analyses_and_transforms_only_where_asked(self):
         _, analysis_config, transformation_config, problem = self._configure(
@@ -189,6 +195,8 @@ def test_a_run_analyses_and_transforms_only_where_asked(self):
             "--transformation=x",
             "--ssaf-analyzer=" + self.analyzer,
             "--ssaf-src-edit-merge=" + self.merge,
+            "--ssaf-report-merge=" + self.report_merge,
+            "--ssaf-conflict-merge=" + self.conflict_merge,
             "--",
             "make",
         )
@@ -202,6 +210,8 @@ def test_both_passes_get_the_clang_and_its_extra_arguments(self):
             "--transformation=x",
             "--ssaf-analyzer=" + self.analyzer,
             "--ssaf-src-edit-merge=" + self.merge,
+            "--ssaf-report-merge=" + self.report_merge,
+            "--ssaf-conflict-merge=" + self.conflict_merge,
             "--ssaf-extra-clang-arg=-DX",
             "--",
             "make",
@@ -353,6 +363,8 @@ def test_a_tool_no_pass_will_run_is_not_resolved_at_all(self):
         workflow_config, _, _, problem = self._configure(
             "--ssaf-analyzer=/no/such/analyzer",
             "--ssaf-src-edit-merge=/no/such/merge",
+            "--ssaf-report-merge=/no/such/report-merge",
+            "--ssaf-conflict-merge=/no/such/conflict-merge",
             "--",
             "make",
         )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
index b4f802fc8ae07..e2db2b86e103e 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
@@ -11,6 +11,7 @@
 from clang_ssaf_orchestrator_make.shims import ld
 from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
 from clang_ssaf_orchestrator_make import build_graph
+from clang_ssaf_orchestrator_make.failure_logs import SSAFFailureKind
 
 from . import configs
 
@@ -149,10 +150,90 @@ def test_a_link_of_nothing_we_know_runs_no_step(self):
     def test_in_the_transformation_pass_the_edits_are_merged_instead(self):
         shim = self._run(["build/a.o"], configs.transformation_config())
         self.assertEqual(
-            ["no edits feed %s, nothing to merge" % self._id("build/prg").get_path()],
+            [
+                "no %s feed %s, nothing to merge"
+                % (what, self._id("build/prg").get_path())
+                for what in ("edits", "transformation reports", "conflict reports")
+            ],
             self.notes,
         )
 
+    def _artifact(self, relative, ssaf_artifact_kind):
+        path = utils.make_parents(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(self._id(relative), ssaf_artifact_kind)
+            )
+        )
+        open(path, "w").close()
+        return path
+
+    def test_the_reports_follow_the_edits_into_the_link(self):
+        a_edits = self._artifact("build/a.o", repository.SSAFArtifactKind.EDITS)
+        a_report = self._artifact("build/a.o", repository.SSAFArtifactKind.REPORT)
+        x_edits = self._artifact("build/libx.a", repository.SSAFArtifactKind.EDITS)
+        x_report = self._artifact("build/libx.a", repository.SSAFArtifactKind.REPORT)
+        x_conflicts = self._artifact(
+            "build/libx.a", repository.SSAFArtifactKind.CONFLICT_REPORT
+        )
+        own_conflicts = self._artifact(
+            "build/prg", repository.SSAFArtifactKind.CONFLICTS
+        )
+        shim = self._run(["build/a.o", "build/libx.a"], configs.transformation_config())
+        prg = self._id("build/prg")
+        self.assertEqual(
+            [
+                (SSAFFailureKind.MERGE, prg),
+                (SSAFFailureKind.TRANSFORMATION_REPORT, prg),
+                (SSAFFailureKind.CONFLICT_REPORT, prg),
+            ],
+            [failure for failure, _ in shim.commands],
+        )
+        _, edits = shim.commands[0]
+        _, report = shim.commands[1]
+        _, conflicts = shim.commands[2]
+        self.assertEqual(
+            ["/bin/clang-ssaf-src-edit-merge", a_edits, x_edits], edits[:3]
+        )
+        self.assertEqual(
+            [
+                "/bin/clang-ssaf-report-merge",
+                a_report,
+                x_report,
+                "-o",
+                self.ssaf_artifact_repo.get_path_for_artifact(
+                    repository.get_ssaf_artifact_id(
+                        prg, repository.SSAFArtifactKind.REPORT
+                    )
+                ),
+            ],
+            report,
+        )
+        self.assertEqual(
+            [
+                "/bin/clang-ssaf-conflict-merge",
+                x_conflicts,
+                own_conflicts,
+                "-o",
+                self.ssaf_artifact_repo.get_path_for_artifact(
+                    repository.get_ssaf_artifact_id(
+                        prg, repository.SSAFArtifactKind.CONFLICT_REPORT
+                    )
+                ),
+            ],
+            conflicts,
+        )
+
+    def test_a_report_is_merged_even_where_no_edits_are(self):
+        self._summary("build/a.o")
+        a_report = self._artifact("build/a.o", repository.SSAFArtifactKind.REPORT)
+        shim = self._run(["build/a.o"], configs.transformation_config())
+        self.assertEqual(
+            [(SSAFFailureKind.TRANSFORMATION_REPORT, self._id("build/prg"))],
+            [failure for failure, _ in shim.commands],
+        )
+        _, report = shim.commands[0]
+        self.assertEqual(a_report, report[1])
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
index 8f4e91edf3fd7..d9aa5780a68a7 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
@@ -88,15 +88,16 @@ def test_the_analysis_result(self):
             ),
         )
 
-    def test_the_transformation_names_every_file_that_holds_conflicts(self):
+    def test_the_transformation_names_the_link_units_edits_and_reports(self):
         self.assertEqual(
             "source-edits: /out/prg.edits.yaml\n"
-            "edit-conflicts: /out/libx.a.conflicts.sarif\n"
-            "edit-conflicts: /out/prg.conflicts.sarif\n",
+            "transformation-report: /out/prg.report.sarif\n"
+            "conflict-report: /out/prg.conflict-report.sarif\n",
             self._stdout(
                 lambda: logging_diagnostics.report_transformation(
                     "/out/prg.edits.yaml",
-                    ["/out/libx.a.conflicts.sarif", "/out/prg.conflicts.sarif"],
+                    "/out/prg.report.sarif",
+                    "/out/prg.conflict-report.sarif",
                 )
             ),
         )
@@ -104,7 +105,7 @@ def test_the_transformation_names_every_file_that_holds_conflicts(self):
             "source-edits: /out/prg.edits.yaml\n",
             self._stdout(
                 lambda: logging_diagnostics.report_transformation(
-                    "/out/prg.edits.yaml", []
+                    "/out/prg.edits.yaml", None, None
                 )
             ),
         )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py
index 90280fb57d7eb..0098dc814ad26 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py
@@ -148,37 +148,25 @@ def test_absent_summary(self):
         self.assertIsNone(self._name())
 
 
-class Conflicts(unittest.TestCase):
+class PresentArtifacts(unittest.TestCase):
     def setUp(self):
         self.tmp = tempfile.mkdtemp()
         self.addCleanup(shutil.rmtree, self.tmp, True)
         self.ssaf_artifact_repo = configs.repository_for(self.tmp)
+        self.report = repository.get_ssaf_artifact_id(
+            build_graph.BuildArtifactId("/p/prg"),
+            repository.SSAFArtifactKind.CONFLICT_REPORT,
+        )
 
-    def _write(self, build_artifact_path, results):
+    def test_an_artifact_on_disk_is_found(self):
         path = utils.make_parents(
-            self.ssaf_artifact_repo.get_path_for_artifact(
-                repository.get_ssaf_artifact_id(
-                    build_graph.BuildArtifactId(build_artifact_path),
-                    repository.SSAFArtifactKind.CONFLICTS,
-                )
-            )
+            self.ssaf_artifact_repo.get_path_for_artifact(self.report)
         )
-        with open(path, "w") as handle:
-            json.dump({"version": "2.1.0", "runs": [{"results": results}]}, handle)
-        return path
-
-    def test_a_merge_that_dropped_nothing_reports_nothing(self):
-        self._write("/p/prg", [])
-        self.assertEqual([], self.ssaf_artifact_repo.get_conflicts())
+        open(path, "w").close()
+        self.assertEqual(path, self.ssaf_artifact_repo.get_path_if_present(self.report))
 
-    def test_every_merge_that_dropped_an_edit_is_reported(self):
-        library = self._write("/p/libx.a", [{"ruleId": "conflict"}])
-        program = self._write("/p/prg", [{"ruleId": "conflict"}])
-        self._write("/p/other", [])
-        self.assertEqual(
-            sorted([library, program]),
-            self.ssaf_artifact_repo.get_conflicts(),
-        )
+    def test_an_artifact_not_on_disk_is_not(self):
+        self.assertIsNone(self.ssaf_artifact_repo.get_path_if_present(self.report))
 
 
 class Counts(unittest.TestCase):



More information about the llvm-branch-commits mailing list