Author: Jan Korous
Date: 2026-10-13T11:54:29-07:00
New Revision: 2250a9708e9e0c203562f4771063684f07ea87dd

URL: 
https://github.com/llvm/llvm-project/commit/2250a9708e9e0c203562f4771063684f07ea87dd
DIFF: 
https://github.com/llvm/llvm-project/commit/2250a9708e9e0c203562f4771063684f07ea87dd.diff

LOG: [clang][ssaf] Add a transformation pass to clang-ssaf-orchestrator-make 
(#231601)

Source edits are generated from the whole-program analysis result, which
exists only once every translation unit has been summarized and the
summaries linked. With --transformation, which requires --analysis, the
tool therefore builds the project a second time after the analysis.

In this pass the clang shim generates source edits and a report for each
compilation, and the ld shim merges the edits of everything a link
consumed with clang-ssaf-src-edit-merge. The run reports the link unit's
merged edits, and any edits a merge dropped because they conflicted.

Edit generation needs the link unit's name, which the linker writes into
the link unit summary during the analysis pass. The tool reads it from
there and passes it on in the second pass's configuration.

Added: 
    
clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass-compile-and-link.test
    clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test

Modified: 
    clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
    clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
    clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
    
clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
    clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
    clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
    
clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py

Removed: 
    


################################################################################
diff  --git 
a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md 
b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
index c815a30b0425d..f11c6d3cd3c48 100644
--- a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
+++ b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
@@ -47,7 +47,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 and its diagnostics
+shim.py          the Shim class, its diagnostics, merge_edits
 commands.py      Files, is_object, artifact_of, summary_of,
                  filter_build_artifacts, unique
 driver_jobs.py   Job and the -### listing reader
@@ -125,9 +125,23 @@ The kind says what went wrong; the kinds are the values of 
`SSAFFailureKind`:
 
 ```text
 extract      summary extraction for a compilation
+transform    source-edit generation for a compilation
 link         clang-ssaf-linker over a link's inputs
+merge        clang-ssaf-src-edit-merge over a link's inputs
 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
 internal     an unhandled exception inside a shim
 ```
+
+## Carrying the link unit name into the second pass
+
+One piece of state flows from the first pass to the second: the top-most link
+unit's own name, which is not an artifact id and is unknown until the first
+pass's linker has written the link unit summary.
+
+The orchestrator reads it from that summary, as the `lu_namespace` component
+whose kind is `LinkUnit`, and puts it in the transformation pass's
+configuration; the clang shim passes it as `--ssaf-link-unit-id=` to every
+source-edit generation. This is why the second pass's configuration file
+cannot be written up front.

diff  --git a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md 
b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
index 1bf14c500979b..e7e788a6fe38c 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
@@ -35,13 +35,16 @@ The mapping onto the SSAF tools is direct:
 
 Run it from the project root; that is where the build runs.
 
-There are two workflows, the second going one step further than the first:
+There are three workflows, each going one step further than the last:
 
 - **Summary extraction and linking**, with `--extract-summaries`. Every
   translation unit gets a summary, and the summaries are linked the way the
   build links the objects, into one link unit summary.
 - **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.
 
 Summary extraction and linking:
 
@@ -62,6 +65,17 @@ $ clang-ssaf-orchestrator-make \
     -- make -j8
 ```
 
+Whole-program transformation, which ends in merged source edits:
+
+```console
+$ clang-ssaf-orchestrator-make \
+    --extract-summaries=PointerFlow,UnsafeBufferUsage \
+    --analysis=UnsafeBufferReachableAnalysisResult \
+    --transformation=cpp-bounded-buffers \
+    -o ssaf-output \
+    -- make -j8
+```
+
 For a project configured by autoconf, run `configure` first and give the
 orchestrator only the `make` invocation.
 
@@ -78,6 +92,9 @@ orchestrator only the `make` invocation.
    * - ``--analysis=<name>``
      - Whole-program analysis to run on the link unit summary. Repeatable.
        Without it the run stops after summary extraction.
+   * - ``--transformation=<name>``
+     - Source transformation to run in the second pass. Requires
+       ``--analysis``. Without it the run stops after the analysis.
    * - ``-o``, ``--output-dir=<dir>``
      - Where the SSAF artifacts go, made absolute. Defaults to
        ``ssaf-output``. It must be absent, empty, or an earlier run's.
@@ -89,6 +106,9 @@ orchestrator only the `make` invocation.
    * - ``--ssaf-analyzer=<path>``
      - Path to ``clang-ssaf-analyzer``. Defaults to the name on ``PATH``, and
        is only required with ``--analysis``.
+   * - ``--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-extra-clang-arg=<arg>``
      - Extra argument for every SSAF compilation. Repeatable.
    * - ``-k``, ``--keep-going``
@@ -135,8 +155,16 @@ translation units, and only the whole path tells them 
apart.
      - Which build artifacts were inputs for the ``<path>`` build artifact.
    * - ``<output-dir>/wpa.json``
      - Whole-program analysis result.
+   * - ``<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.
+   * - ``<output-dir>/ssaf-artifacts/<path>.report.sarif``
+     - Transformation report for the ``<path>`` build artifact.
    * - ``<output-dir>/config-analysis.json``
-     - Configuration for the analysis pass.
+     - Configuration for the analysis pass, the first build pass.
+   * - ``<output-dir>/config-transformation.json``
+     - Configuration for the transformation pass, the second build pass.
    * - ``<output-dir>/shims/``
      - Links to the shims named so they interpose the binary tools, recreated
        on every run.

diff  --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test 
b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
index 7796cbc785840..3fefaae801bce 100644
--- a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
@@ -7,10 +7,12 @@
 # CHECK: --extract-summaries
 # CHECK: --ssaf-clang
 # CHECK: --analysis
+# CHECK: --transformation
 
 # RUN: %clang-ssaf-orchestrator-make --help | FileCheck %s --check-prefix=SSAF
 # SSAF: --ssaf-linker
 # SSAF: --ssaf-analyzer
+# SSAF: --ssaf-src-edit-merge
 # SSAF: --ssaf-extra-clang-arg
 # SSAF: --keep-going
 # SSAF: --verbose

diff  --git 
a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass-compile-and-link.test
 
b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass-compile-and-link.test
new file mode 100644
index 0000000000000..ed44164de92e4
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass-compile-and-link.test
@@ -0,0 +1,44 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# 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: summaries: 3
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+# STDOUT: source-edits: {{[^ ]*}}/out/ssaf-artifacts{{[^ 
]*}}/build/prg.edits.yaml
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/prg.edits.yaml \
+# RUN:   | FileCheck %s --check-prefix=EDITS
+# EDITS: Replacements:
+# EDITS-DAG: FilePath: {{.*}}src/main.cpp
+# EDITS-DAG: FilePath: {{.*}}src/parse.cpp
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/prg: src/main.cpp src/parse.cpp
+       $(CXX) -o $@ src/main.cpp src/parse.cpp
+
+#--- src/parse.cpp
+int total(int *data, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += data[i];
+  return t;
+}
+
+#--- src/main.cpp
+int total(int *data, int n);
+int main() {
+  int values[4] = {1, 2, 3, 4};
+  return total(values, 4);
+}

diff  --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test 
b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test
new file mode 100644
index 0000000000000..c9b19bd5ac7b2
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/two-pass.test
@@ -0,0 +1,67 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# 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: summaries: 3
+# 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
+
+# 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 \
+# RUN:      %t/out/ssaf-artifacts%t/build/parse.o.edits.yaml
+# RUN: ls %t/out/ssaf-artifacts%t/build/main.o.report.sarif \
+# RUN:      %t/out/ssaf-artifacts%t/build/parse.o.report.sarif
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/prg.edits.yaml | FileCheck %s 
--check-prefix=EDITS
+# EDITS: Replacements:
+# EDITS-DAG: FilePath: {{.*}}src/main.cpp
+# EDITS-DAG: FilePath: {{.*}}src/parse.cpp
+# EDITS-DAG: ReplacementText: {{.*}}bounded_ptr<int>
+
+# Both configuration files are written, one per pass.
+# RUN: FileCheck %s --check-prefix=PASS1 \
+# RUN:   --input-file=%t/out/config-analysis.json
+# PASS1: "pass_kind": "analysis"
+# RUN: FileCheck %s --check-prefix=PASS2 \
+# RUN:   --input-file=%t/out/config-transformation.json
+# PASS2-DAG: "pass_kind": "transformation"
+# PASS2-DAG: "link_unit_id":
+
+# The second pass carries the link unit name the first pass recovered.
+# RUN: FileCheck %s --check-prefix=LUID \
+# RUN:   --input-file=%t/out/config-transformation.json
+# LUID: "link_unit_id": "{{.+}}"
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/%.o: src/%.cpp
+       $(CXX) -c -o $@ $<
+build/prg: build/main.o build/parse.o
+       $(CXX) -o $@ build/main.o build/parse.o
+
+#--- src/parse.cpp
+int total(int *data, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += data[i];
+  return t;
+}
+
+#--- src/main.cpp
+int total(int *data, int n);
+int main() {
+  int values[4] = {1, 2, 3, 4};
+  return total(values, 4);
+}

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 01caf368092dd..d2532c6ee8062 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
@@ -50,6 +50,7 @@ def output_dir_marker_path(output_dir):
 
 class SSAFPassKind(str, enum.Enum):
     ANALYSIS = "analysis"
+    TRANSFORMATION = "transformation"
 
 
 class SSAFPassConfig:
@@ -84,6 +85,26 @@ def __init__(
         self.analyses = list(analyses)
 
 
+class SSAFTransformationPassConfig(SSAFPassConfig):
+    pass_kind = SSAFPassKind.TRANSFORMATION
+
+    def __init__(
+        self,
+        ssaf_clang=None,
+        ssaf_extra_clang_args=(),
+        ssaf_src_edit_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.transformation = transformation
+        self.link_unit_id = link_unit_id
+
+    def name_link_unit(self, link_unit_id):
+        self.link_unit_id = link_unit_id
+
+
 class SSAFWorkflowConfig:
     def __init__(
         self,

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 a9c096ba3208f..f47b4f05cc73e 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
@@ -116,12 +116,27 @@ def configure(command_line_opts, env):
         if command_line_opts.analyses
         else None
     )
+    ssaf_src_edit_merge = (
+        _resolve_ssaf_tool(
+            "--ssaf-src-edit-merge",
+            "clang-ssaf-src-edit-merge",
+            command_line_opts.ssaf_src_edit_merge,
+            installed_shim_dir_path,
+            errors,
+        )
+        if command_line_opts.transformation
+        else None
+    )
 
     if errors:
         return None, options.Problem(
             options.ENVIRONMENT_ERROR, "required tools not resolvable:", errors
         )
 
+    common_pass_config = dict(
+        ssaf_clang=ssaf_clang,
+        ssaf_extra_clang_args=command_line_opts.ssaf_extra_clang_args,
+    )
     workflow_config = config.SSAFWorkflowConfig(
         output_dir=output_dir,
         installed_shim_dir_path=installed_shim_dir_path,
@@ -133,10 +148,18 @@ def configure(command_line_opts, env):
         build_environment=env,
     )
     analysis_config = config.SSAFAnalysisPassConfig(
-        ssaf_clang=ssaf_clang,
-        ssaf_extra_clang_args=command_line_opts.ssaf_extra_clang_args,
         extractors=command_line_opts.extract_summaries,
         ssaf_linker=ssaf_linker,
         analyses=command_line_opts.analyses,
+        **common_pass_config,
+    )
+    transformation_config = (
+        config.SSAFTransformationPassConfig(
+            ssaf_src_edit_merge=ssaf_src_edit_merge,
+            transformation=command_line_opts.transformation,
+            **common_pass_config,
+        )
+        if command_line_opts.transformation
+        else None
     )
-    return (workflow_config, analysis_config), None
+    return (workflow_config, analysis_config, transformation_config), None

diff  --git 
a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
 
b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
index bfed688447ceb..abc228dfb1c27 100644
--- 
a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
+++ 
b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
@@ -18,7 +18,9 @@ def from_json(written):
         for key, value in written.items()
         if key not in shared and key != "pass_kind"
     }
-    config.SSAFPassKind(written["pass_kind"])
+    pass_kind = config.SSAFPassKind(written["pass_kind"])
+    if pass_kind == config.SSAFPassKind.TRANSFORMATION:
+        return workflow_config, config.SSAFTransformationPassConfig(**own)
     return workflow_config, config.SSAFAnalysisPassConfig(**own)
 
 

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 fc242f39dc8fd..84f52c5b3f9fe 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
@@ -14,7 +14,9 @@
 
 class SSAFFailureKind(enum.Enum):
     EXTRACT = "extract"
+    TRANSFORM = "transform"
     LINK = "link"
+    MERGE = "merge"
     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 771da5cb22b87..96c6ff3293b63 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
@@ -39,3 +39,9 @@ def report_summaries(summaries, link_unit, summary_path):
 
 def report_analysis_result(wpa_result_path):
     report_info("analysis-result", wpa_result_path)
+
+
+def report_transformation(merged, conflicts_paths):
+    report_info("source-edits", merged)
+    for conflicts_path in conflicts_paths:
+        report_info("edit-conflicts", conflicts_path)

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 4650432edd9ca..3a962f6274be0 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
@@ -54,6 +54,13 @@ def build_parser():
         help="Whole-program analysis to run on the link unit summary. "
         "Repeatable. Without it the run stops after summary extraction.",
     )
+    parser.add_argument(
+        "--transformation",
+        metavar="<name>",
+        default=None,
+        help="Source transformation to run in the second pass. Requires "
+        "--analysis. Without it the run stops after the analysis.",
+    )
     parser.add_argument(
         "--ssaf-linker",
         metavar="<path>",
@@ -66,6 +73,12 @@ def build_parser():
         default=None,
         help="Path to clang-ssaf-analyzer (default: found on PATH).",
     )
+    parser.add_argument(
+        "--ssaf-src-edit-merge",
+        metavar="<path>",
+        default=None,
+        help="Path to clang-ssaf-src-edit-merge (default: found on PATH).",
+    )
     parser.add_argument(
         "--ssaf-extra-clang-arg",
         metavar="<arg>",
@@ -102,10 +115,12 @@ def __init__(
         output_dir=None,
         extract_summaries=None,
         analyses=(),
+        transformation=None,
         ssaf_clang=None,
         ssaf_extra_clang_args=(),
         ssaf_linker=None,
         ssaf_analyzer=None,
+        ssaf_src_edit_merge=None,
         keep_going=False,
         verbose=False,
     ):
@@ -113,10 +128,12 @@ def __init__(
         self.output_dir = output_dir
         self.extract_summaries = extract_summaries
         self.analyses = list(analyses)
+        self.transformation = transformation
         self.ssaf_clang = ssaf_clang
         self.ssaf_extra_clang_args = list(ssaf_extra_clang_args)
         self.ssaf_linker = ssaf_linker
         self.ssaf_analyzer = ssaf_analyzer
+        self.ssaf_src_edit_merge = ssaf_src_edit_merge
         self.keep_going = keep_going
         self.verbose = verbose
 
@@ -130,16 +147,25 @@ def resolve(argv):
     if not build_command:
         return None, Problem(USAGE_ERROR, "no build command given")
 
+    if args.transformation and not args.analyses:
+        return None, Problem(
+            USAGE_ERROR,
+            "--transformation needs --analysis: the transformation consumes "
+            "the whole-program analysis result",
+        )
+
     return (
         CommandLineOpts(
             build_command=build_command,
             output_dir=args.output_dir,
             extract_summaries=args.extract_summaries,
             analyses=args.analyses,
+            transformation=args.transformation,
             ssaf_clang=args.ssaf_clang,
             ssaf_extra_clang_args=args.ssaf_extra_clang_args,
             ssaf_linker=args.ssaf_linker,
             ssaf_analyzer=args.ssaf_analyzer,
+            ssaf_src_edit_merge=args.ssaf_src_edit_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 eb5470da8820d..ce2a73cbebb0b 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
@@ -89,7 +89,7 @@ def clear_output_dir(output_dir):
 
 
 def _no_link_unit(message):
-    return None, None, options.Problem(1, message)
+    return None, None, None, options.Problem(1, message)
 
 
 def get_topmost_link_unit(ssaf_artifact_repo, output_dir):
@@ -115,7 +115,13 @@ def get_topmost_link_unit(ssaf_artifact_repo, output_dir):
             "or failed" % build_artifact_id.get_path()
         )
     summary = ssaf_artifact_repo.get_path_for_artifact(ssaf_artifact_id)
-    return build_artifact_id, summary, None
+    unit_id = ssaf_artifact_repo.get_link_unit_name(summary)
+    if unit_id is None:
+        return _no_link_unit(
+            "'%s' is not a link unit; whole-program analysis needs one, so "
+            "build a target that links" % build_artifact_id.get_path()
+        )
+    return build_artifact_id, summary, unit_id, None
 
 
 def run_ssaf_analysis_pass(
@@ -132,7 +138,7 @@ def run_ssaf_analysis_pass(
     if summaries == 0:
         return options.Problem(1, "no summaries were extracted")
 
-    link_unit, summary, problem = get_topmost_link_unit(
+    link_unit, summary, _, problem = get_topmost_link_unit(
         ssaf_artifact_repo, workflow_config.output_dir
     )
     if problem is not None:
@@ -162,7 +168,39 @@ def run_ssaf_analysis_pass(
     return None
 
 
-def run_ssaf_workflow(workflow_config, analysis_config):
+def run_ssaf_transformation_pass(
+    workflow_config, transformation_config, ssaf_artifact_repo, 
shim_link_dir_path
+):
+    link_unit, _, unit_id, problem = get_topmost_link_unit(
+        ssaf_artifact_repo, workflow_config.output_dir
+    )
+    if problem is not None:
+        return problem
+
+    transformation_config.name_link_unit(unit_id)
+    serialized_config_path = write_pass_config(workflow_config, 
transformation_config)
+    problem = run_build_with_toolchain_interposition(
+        workflow_config,
+        serialized_config_path,
+        shim_link_dir_path,
+        "rebuild for the transformation pass",
+    )
+    if problem is not None:
+        return problem
+
+    edits = get_ssaf_artifact_id(link_unit, SSAFArtifactKind.EDITS)
+    if not ssaf_artifact_repo.has_artifact(edits):
+        return options.Problem(
+            1, "no merged source edits for '%s'" % link_unit.get_path()
+        )
+    logging_diagnostics.report_transformation(
+        ssaf_artifact_repo.get_path_for_artifact(edits),
+        ssaf_artifact_repo.get_conflicts(),
+    )
+    return None
+
+
+def run_ssaf_workflow(workflow_config, analysis_config, transformation_config):
     warn_if_not_make(workflow_config.build_command)
 
     ssaf_artifact_repo = SSAFArtifactRepository(
@@ -186,6 +224,16 @@ def run_ssaf_workflow(workflow_config, analysis_config):
     if problem is not None:
         return problem
 
+    if transformation_config is not None:
+        problem = run_ssaf_transformation_pass(
+            workflow_config,
+            transformation_config,
+            ssaf_artifact_repo,
+            shim_link_dir_path,
+        )
+        if problem is not None:
+            return problem
+
     logging_diagnostics.report_output_dir(workflow_config.output_dir)
 
     failures = failure_logs.count_failure_logs(workflow_config.output_dir)

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 7525dfc22b7ee..f00ae9bf62767 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
@@ -6,7 +6,7 @@
 import re
 import subprocess
 
-from .. import logging_diagnostics, utils
+from .. import config, logging_diagnostics, utils
 from ..build_graph import BuildArtifactId
 from ..failure_logs import SSAFFailureKind
 from ..ssaf_artifact_repository import SSAFArtifactKind, get_ssaf_artifact_id
@@ -93,6 +93,30 @@ def _summary_extraction_args(shim, build_artifact_id):
     return summary, extra + shim.pass_config.ssaf_extra_clang_args
 
 
+def _transformation_args(shim, build_artifact_id):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    edits = utils.make_parents(
+        ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.EDITS)
+        )
+    )
+    extra = [
+        "--ssaf-source-transformation=" + shim.pass_config.transformation,
+        "--ssaf-global-scope-analysis-result="
+        + config.wpa_result_path(shim.workflow_config.output_dir),
+        "--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)
+            )
+        ),
+    ]
+    return edits, extra + shim.pass_config.ssaf_extra_clang_args
+
+
 def _run_clang_driven_tool(shim, cwd):
     job = Job(list(shim.argv[1:]))
 
@@ -104,10 +128,15 @@ def _run_clang_driven_tool(shim, cwd):
 
     output = os.path.join(cwd, job.output)
     build_artifact_id = BuildArtifactId(output)
-    artifact, extra = _summary_extraction_args(shim, build_artifact_id)
+    if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
+        artifact, extra = _transformation_args(shim, build_artifact_id)
+        failure_kind = SSAFFailureKind.TRANSFORM
+    else:
+        artifact, 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, SSAFFailureKind.EXTRACT, build_artifact_id):
+    if not shim.run(ssaf_command, failure_kind, build_artifact_id):
         try:
             os.remove(artifact)
         except OSError:

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 2eaf808e87095..d4794cb4e734c 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
@@ -32,9 +32,13 @@ def filter_build_artifacts(build_graph, ssaf_artifact_repo, 
cwd, files):
     kept = []
     for each in files:
         build_artifact_id = BuildArtifactId(os.path.join(cwd, each))
-        if artifact_of(
-            ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.SUMMARY
-        ) or build_graph.was_built(build_artifact_id):
+        if (
+            artifact_of(ssaf_artifact_repo, build_artifact_id, 
SSAFArtifactKind.SUMMARY)
+            or artifact_of(
+                ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.EDITS
+            )
+            or build_graph.was_built(build_artifact_id)
+        ):
             kept.append(build_artifact_id)
     return kept
 

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 d7fe1d65ccd72..54c5c759ed5bf 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
@@ -4,7 +4,7 @@
 
 import os
 
-from .. import logging_diagnostics, utils
+from .. import config, logging_diagnostics, utils
 from ..build_graph import BuildArtifactId
 from ..failure_logs import SSAFFailureKind
 from ..ssaf_artifact_repository import SSAFArtifactKind, get_ssaf_artifact_id
@@ -16,6 +16,7 @@
     summary_of,
     unique,
 )
+from .shim import merge_edits
 
 
 def identify_inputs_output(argv):
@@ -77,6 +78,10 @@ def invoke_ssaf_tools(shim, cwd):
         shim.build_graph, ssaf_artifact_repo, cwd, files.inputs
     )
 
+    if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
+        merge_edits(shim, build_artifact_id, input_build_artifact_ids)
+        return
+
     shim.build_graph.record_build_step(
         build_artifact_id,
         inputs=input_build_artifact_ids,

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 114135950896b..5180bced1ab30 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
@@ -11,7 +11,12 @@
 
 from .. import build_graph, config, failure_logs, logging_diagnostics, utils
 from ..failure_logs import SSAFFailureKind
-from ..ssaf_artifact_repository import SSAFArtifactRepository
+from ..ssaf_artifact_repository import (
+    SSAFArtifactKind,
+    SSAFArtifactRepository,
+    get_ssaf_artifact_id,
+)
+from .commands import artifact_of, unique
 
 SKIP_BINARY_TOOL_INVOCATION = "--ssaf-shim-skip-binary-tool-invocation"
 
@@ -181,3 +186,35 @@ def protect(self, work):
                 logging_diagnostics.report_error("and its log could not be 
written")
         # A shim must never be the reason a build fails.
         return 0
+
+
+def merge_edits(shim, build_artifact_id, input_build_artifact_ids):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    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(),
+        )
+        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)
+            )
+        ),
+    ]
+    command += [
+        "--sarif-conflicts-out="
+        + ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.CONFLICTS)
+        )
+    ]
+    shim.run(command, SSAFFailureKind.MERGE, build_artifact_id)

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 674d5c4ccbbf6..5d920673350cf 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
@@ -6,11 +6,14 @@
 import enum
 import os
 
-from .utils import coalesce_root_path, find_with_suffix
+from .utils import coalesce_root_path, find_with_suffix, read_json
 
 
 class SSAFArtifactKind(enum.Enum):
     SUMMARY = ".summary.json"
+    EDITS = ".edits.yaml"
+    CONFLICTS = ".conflicts.sarif"
+    REPORT = ".report.sarif"
 
 
 class SSAFArtifactId:
@@ -54,5 +57,22 @@ def has_artifact(self, ssaf_artifact_id):
     def _all_paths_of_kind(self, ssaf_artifact_kind):
         return find_with_suffix(self.ssaf_artifact_dir, 
ssaf_artifact_kind.value)
 
+    def get_link_unit_name(self, summary_path):
+        document = read_json(summary_path)
+        if document is None:
+            return None
+        for component in document.get("lu_namespace", []):
+            if component.get("kind") == "LinkUnit":
+                return component.get("name")
+        return None
+
     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 9a359514de7bc..f95799c68e648 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
@@ -21,6 +21,21 @@ def analysis_config(**overrides):
     return config.SSAFAnalysisPassConfig(**fields)
 
 
+def transformation_config(**overrides):
+    fields = dict(
+        ssaf_clang="/bin/clang",
+        ssaf_extra_clang_args=[],
+        ssaf_src_edit_merge="/bin/clang-ssaf-src-edit-merge",
+        transformation="x",
+    )
+    fields.update(overrides)
+    return config.SSAFTransformationPassConfig(**fields)
+
+
+def pass_configs():
+    return analysis_config(), transformation_config(link_unit_id="build/prg")
+
+
 def workflow_config(env=None, output_dir="/out", verbose=False):
     env = env or {}
     return config.SSAFWorkflowConfig(

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 720d8bcbe089f..2ef092395421b 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
@@ -148,6 +148,20 @@ def 
test_extraction_names_the_unit_and_where_its_summary_goes(self):
         self.assertIn("--ssaf-tu-summary-file=" + summary, extra)
         self.assertTrue(os.path.isdir(os.path.dirname(summary)))
 
+    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"))
+        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)
+        )
+
     def test_the_extra_clang_arguments_come_last(self):
         shim = 
self._shim(configs.analysis_config(ssaf_extra_clang_args=["-DX"]))
         _, extra = clang._summary_extraction_args(shim, self._id("build/a.o"))

diff  --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py 
b/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
index 6990f7c304c5c..f7d28f2af2a8a 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
@@ -100,6 +100,10 @@ def test_a_compiled_object_is_kept(self):
         self._write("build/a.o", repository.SSAFArtifactKind.SUMMARY)
         self.assertEqual([self._id("build/a.o")], self._kept("build/a.o"))
 
+    def test_an_object_with_only_source_edits_is_kept(self):
+        self._write("build/a.o", repository.SSAFArtifactKind.EDITS)
+        self.assertEqual([self._id("build/a.o")], self._kept("build/a.o"))
+
     def test_an_assembled_object_with_only_a_record_is_kept(self):
         self.build_graph.record_build_step(
             self._id("build/combined.o"), [self._id("build/a.o")]

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 e0aafdc425bcd..c6dbc9cd6341b 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
@@ -13,6 +13,9 @@
 class Passes(unittest.TestCase):
     def test_a_pass_names_its_own_configuration_file(self):
         self.assertEqual("config-analysis.json", 
configs.analysis_config().file_name)
+        self.assertEqual(
+            "config-transformation.json", 
configs.transformation_config().file_name
+        )
 
     def test_a_pass_kind_is_stored_as_its_plain_string(self):
         written = json.loads(json.dumps(configs.analysis_config().to_json()))
@@ -30,10 +33,36 @@ def test_a_pass_carries_only_the_tools_it_runs(self):
             },
             set(configs.analysis_config().to_json()),
         )
+        self.assertEqual(
+            {
+                "pass_kind",
+                "ssaf_clang",
+                "ssaf_extra_clang_args",
+                "ssaf_src_edit_merge",
+                "transformation",
+                "link_unit_id",
+            },
+            set(configs.transformation_config().to_json()),
+        )
 
     def test_a_pass_knows_nothing_of_the_run_it_belongs_to(self):
         run_only = config.SSAFWorkflowConfig.written_field_names()
-        self.assertEqual(set(), run_only & 
set(configs.analysis_config().to_json()))
+        for pass_config in configs.pass_configs():
+            self.assertEqual(set(), run_only & set(pass_config.to_json()), 
pass_config)
+
+    def test_both_passes_compile_so_both_carry_the_clang(self):
+        for pass_config in (
+            configs.analysis_config(ssaf_extra_clang_args=["-DX"]),
+            configs.transformation_config(ssaf_extra_clang_args=["-DX"]),
+        ):
+            self.assertEqual("/bin/clang", pass_config.ssaf_clang)
+            self.assertEqual(["-DX"], pass_config.ssaf_extra_clang_args)
+
+    def test_the_link_unit_is_named_after_the_analysis_build(self):
+        transformation_config = configs.transformation_config()
+        self.assertIsNone(transformation_config.link_unit_id)
+        transformation_config.name_link_unit("build/prg")
+        self.assertEqual("build/prg", transformation_config.link_unit_id)
 
 
 class OutputDirPaths(unittest.TestCase):
@@ -82,5 +111,24 @@ def 
test_an_absent_path_is_empty_so_the_live_path_is_never_searched(self):
         self.assertEqual("", config.SSAFWorkflowConfig().restored_path_env_var)
 
 
+class Serialization(unittest.TestCase):
+    def test_each_pass_file_carries_only_that_pass(self):
+        analysis_config, transformation_config = configs.pass_configs()
+        first = analysis_config.to_json()
+        second = transformation_config.to_json()
+        self.assertEqual(
+            {"extractors", "ssaf_linker", "analyses"}, set(first) - set(second)
+        )
+        self.assertEqual(
+            {"ssaf_src_edit_merge", "transformation", "link_unit_id"},
+            set(second) - set(first),
+        )
+        self.assertEqual(
+            {"pass_kind", "ssaf_clang", "ssaf_extra_clang_args"},
+            set(first) & set(second),
+        )
+        self.assertNotEqual(first["pass_kind"], second["pass_kind"])
+
+
 if __name__ == "__main__":
     unittest.main()

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 6a512c24bf9dd..3ed1e13e2b154 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
@@ -114,6 +114,7 @@ def setUp(self):
         self.clang = self._executable("clang")
         self.linker = self._executable("clang-ssaf-linker")
         self.analyzer = self._executable("clang-ssaf-analyzer")
+        self.merge = self._executable("clang-ssaf-src-edit-merge")
         self.project = os.path.join(self.tmp, "project")
         os.makedirs(self.project)
         self.addCleanup(os.chdir, os.getcwd())
@@ -139,10 +140,12 @@ def _configure_argv(self, argv, env=None):
         configs, problem = config_orchestrator.configure(
             command_line_opts, {} if env is None else env
         )
-        return (configs or (None, None)) + (problem,)
+        return (configs or (None, None, None)) + (problem,)
 
     def test_a_valid_command_line_yields_the_configuration(self):
-        workflow_config, analysis_config, problem = self._configure("--", 
"make", "-j8")
+        workflow_config, analysis_config, _, problem = self._configure(
+            "--", "make", "-j8"
+        )
         self.assertIsNone(problem)
         self.assertEqual(["make", "-j8"], workflow_config.build_command)
         self.assertEqual("PointerFlow", analysis_config.extractors)
@@ -154,19 +157,62 @@ def 
test_the_configuration_holds_the_resolved_tool_not_what_was_typed(self):
             "--ssaf-clang=clang",
             "--ssaf-linker=clang-ssaf-linker",
             "--ssaf-analyzer=clang-ssaf-analyzer",
+            "--ssaf-src-edit-merge=clang-ssaf-src-edit-merge",
             "--analysis=a",
+            "--transformation=x",
             "--",
             "make",
         ]
         with mock.patch.dict(os.environ, {"PATH": self.tmp}):
-            workflow_config, analysis_config, problem = 
self._configure_argv(argv)
+            (
+                workflow_config,
+                analysis_config,
+                transformation_config,
+                problem,
+            ) = self._configure_argv(argv)
         self.assertIsNone(problem)
         self.assertEqual(self.clang, analysis_config.ssaf_clang)
         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)
+
+    def test_a_run_analyses_and_transforms_only_where_asked(self):
+        _, analysis_config, transformation_config, problem = self._configure(
+            "--", "make"
+        )
+        self.assertIsNone(problem)
+        self.assertIsInstance(analysis_config, config.SSAFAnalysisPassConfig)
+        self.assertIsNone(transformation_config)
+
+        _, analysis_config, transformation_config, problem = self._configure(
+            "--analysis=a",
+            "--transformation=x",
+            "--ssaf-analyzer=" + self.analyzer,
+            "--ssaf-src-edit-merge=" + self.merge,
+            "--",
+            "make",
+        )
+        self.assertIsNone(problem)
+        self.assertEqual(["a"], analysis_config.analyses)
+        self.assertEqual("x", transformation_config.transformation)
+
+    def test_both_passes_get_the_clang_and_its_extra_arguments(self):
+        _, analysis_config, transformation_config, problem = self._configure(
+            "--analysis=a",
+            "--transformation=x",
+            "--ssaf-analyzer=" + self.analyzer,
+            "--ssaf-src-edit-merge=" + self.merge,
+            "--ssaf-extra-clang-arg=-DX",
+            "--",
+            "make",
+        )
+        self.assertIsNone(problem)
+        for pass_config in (analysis_config, transformation_config):
+            self.assertEqual(self.clang, pass_config.ssaf_clang)
+            self.assertEqual(["-DX"], pass_config.ssaf_extra_clang_args)
 
     def test_the_environment_reaches_the_config_but_not_the_file(self):
-        workflow_config, _, problem = self._configure(
+        workflow_config, _, _, problem = self._configure(
             "--", "make", env={"PATH": "/usr/bin", "SECRET": "x"}
         )
         self.assertIsNone(problem)
@@ -176,7 +222,7 @@ def 
test_the_environment_reaches_the_config_but_not_the_file(self):
 
     def test_an_output_dir_holding_the_project_is_refused(self):
         for value in (".", "..", os.getcwd()):
-            workflow_config, _, problem = self._configure("-o", value, "--", 
"make")
+            workflow_config, _, _, problem = self._configure("-o", value, 
"--", "make")
             self.assertIsNone(workflow_config, value)
             self.assertEqual(options.ENVIRONMENT_ERROR, problem.exit_status)
             self.assertIn("would contain the project", problem.message)
@@ -185,7 +231,7 @@ def test_a_directory_no_run_made_is_not_deleted(self):
         other = os.path.join(self.project, "src")
         os.makedirs(other)
         open(os.path.join(other, "main.c"), "w").close()
-        workflow_config, _, problem = self._configure("-o", other, "--", 
"make")
+        workflow_config, _, _, problem = self._configure("-o", other, "--", 
"make")
         self.assertIsNone(workflow_config)
         self.assertEqual(options.ENVIRONMENT_ERROR, problem.exit_status)
         self.assertIn("no earlier run made it", problem.message)
@@ -194,7 +240,7 @@ def test_a_directory_no_run_made_is_not_deleted(self):
     def test_an_empty_directory_is_taken(self):
         empty = os.path.join(self.tmp, "empty")
         os.makedirs(empty)
-        workflow_config, _, problem = self._configure("-o", empty, "--", 
"make")
+        workflow_config, _, _, problem = self._configure("-o", empty, "--", 
"make")
         self.assertIsNone(problem)
         self.assertEqual(os.path.realpath(empty), workflow_config.output_dir)
 
@@ -202,18 +248,20 @@ def test_a_directory_an_earlier_run_made_is_taken(self):
         earlier = os.path.join(self.tmp, "earlier")
         os.makedirs(os.path.join(earlier, "ssaf-artifacts"))
         open(config.output_dir_marker_path(earlier), "w").close()
-        _, _, problem = self._configure("-o", earlier, "--", "make")
+        _, _, _, problem = self._configure("-o", earlier, "--", "make")
         self.assertIsNone(problem)
 
     def test_a_relative_output_dir_is_made_absolute(self):
-        workflow_config, _, problem = self._configure("-o", "somewhere", "--", 
"make")
+        workflow_config, _, _, problem = self._configure(
+            "-o", "somewhere", "--", "make"
+        )
         self.assertIsNone(problem)
         self.assertEqual(
             os.path.join(os.getcwd(), "somewhere"), workflow_config.output_dir
         )
 
     def test_an_unresolvable_ssaf_tool(self):
-        workflow_config, _, problem = self._configure_argv(
+        workflow_config, _, _, problem = self._configure_argv(
             [
                 "--extract-summaries=PointerFlow",
                 "--ssaf-clang=/no/such/clang",
@@ -228,7 +276,7 @@ def test_an_unresolvable_ssaf_tool(self):
         self.assertIn("/no/such/clang", problem.details[0])
 
     def test_the_ssaf_tools_take_no_flags(self):
-        workflow_config, _, problem = self._configure_argv(
+        workflow_config, _, _, problem = self._configure_argv(
             [
                 "--extract-summaries=PointerFlow",
                 "--ssaf-clang=%s -O2" % self.clang,
@@ -257,7 +305,7 @@ def test_a_detail_names_the_option_and_the_path(self):
 
     def test_a_tool_absent_from_path_names_what_was_looked_for(self):
         with mock.patch.dict(os.environ, {"PATH": self.elsewhere}):
-            workflow_config, _, problem = self._configure_argv(
+            workflow_config, _, _, problem = self._configure_argv(
                 [
                     "--extract-summaries=PointerFlow",
                     "--ssaf-clang=" + self.clang,
@@ -272,7 +320,7 @@ def 
test_a_tool_absent_from_path_names_what_was_looked_for(self):
         )
 
     def test_every_unresolvable_tool_is_reported_at_once(self):
-        workflow_config, _, problem = self._configure_argv(
+        workflow_config, _, _, problem = self._configure_argv(
             [
                 "--extract-summaries=PointerFlow",
                 "--ssaf-clang=/no/such/clang",
@@ -294,13 +342,19 @@ def 
test_every_unresolvable_tool_is_reported_at_once(self):
         )
 
     def test_the_analyzer_is_only_required_when_an_analysis_is_asked_for(self):
-        workflow_config, _, problem = self._configure("--", "make")
+        workflow_config, _, transformation_config, problem = self._configure(
+            "--", "make"
+        )
         self.assertIsNone(problem)
         self.assertIsNone(workflow_config.ssaf_analyzer)
+        self.assertIsNone(transformation_config)
 
     def test_a_tool_no_pass_will_run_is_not_resolved_at_all(self):
-        workflow_config, _, problem = self._configure(
-            "--ssaf-analyzer=/no/such/analyzer", "--", "make"
+        workflow_config, _, _, problem = self._configure(
+            "--ssaf-analyzer=/no/such/analyzer",
+            "--ssaf-src-edit-merge=/no/such/merge",
+            "--",
+            "make",
         )
         self.assertIsNone(problem)
         self.assertIsNone(workflow_config.ssaf_analyzer)

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 2b2e5ec35c146..a6ae6c1872375 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
@@ -127,6 +127,13 @@ def test_a_link_of_nothing_we_know_runs_no_step(self):
         self.assertEqual([], shim.missing)
         self.assertIn("nothing to link", self.notes[-1])
 
+    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()],
+            self.notes,
+        )
+
 
 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 18b1ef57533e2..8f4e91edf3fd7 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,6 +88,27 @@ def test_the_analysis_result(self):
             ),
         )
 
+    def test_the_transformation_names_every_file_that_holds_conflicts(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",
+            self._stdout(
+                lambda: logging_diagnostics.report_transformation(
+                    "/out/prg.edits.yaml",
+                    ["/out/libx.a.conflicts.sarif", 
"/out/prg.conflicts.sarif"],
+                )
+            ),
+        )
+        self.assertEqual(
+            "source-edits: /out/prg.edits.yaml\n",
+            self._stdout(
+                lambda: logging_diagnostics.report_transformation(
+                    "/out/prg.edits.yaml", []
+                )
+            ),
+        )
+
     def test_results_are_flushed_as_they_are_written(self):
         flushes = []
         stream = io.StringIO()

diff  --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py 
b/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
index a1ac1cbdf4eae..edb027f91d114 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
@@ -49,6 +49,12 @@ def test_a_separator_with_nothing_after_it_is_the_same(self):
         self.assertIsNone(command_line_opts)
         self.assertEqual(options.USAGE_ERROR, problem.exit_status)
 
+    def test_a_transformation_needs_an_analysis(self):
+        command_line_opts, problem = self._resolve("--transformation=x", "--", 
"make")
+        self.assertIsNone(command_line_opts)
+        self.assertEqual(options.USAGE_ERROR, problem.exit_status)
+        self.assertIn("--analysis", problem.message)
+
     def test_resolving_makes_none_of_the_calls_configure_makes(self):
         # Not every filesystem call: argparse reads the locale through
         # gettext. These are the calls the configuration layer makes, and the

diff  --git 
a/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py 
b/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
index 1b967192f9835..9a527d8d98c2e 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
@@ -87,8 +87,8 @@ def test_an_environment_error_stops_before_the_build(self):
         self.assertIn("--ssaf-clang", message)
         self.assertEqual([], self.run_calls)
 
-    def test_a_configured_run_is_handed_its_two_configurations(self):
-        configured = ("workflow", "analysis")
+    def test_a_configured_run_is_handed_its_three_configurations(self):
+        configured = ("workflow", "analysis", "transformation")
         with mock.patch.object(
             config_orchestrator, "configure", return_value=(configured, None)
         ):
@@ -125,21 +125,37 @@ def setUp(self):
         )
         self.analysis_config = config.SSAFAnalysisPassConfig(
             ssaf_clang="/bin/clang",
+            ssaf_extra_clang_args=["-DX"],
             extractors="PointerFlow",
             ssaf_linker="/bin/clang-ssaf-linker",
+            analyses=["A"],
         )
-
-    def test_a_pass_is_written_under_the_output_directory(self):
-        path = orchestrator.write_pass_config(
-            self.workflow_config, self.analysis_config
+        self.transformation_config = config.SSAFTransformationPassConfig(
+            ssaf_clang="/bin/clang",
+            ssaf_extra_clang_args=["-DX"],
+            ssaf_src_edit_merge="/bin/clang-ssaf-src-edit-merge",
+            transformation="x",
         )
-        self.assertEqual(os.path.join(self.tmp, 
self.analysis_config.file_name), path)
-        self.assertTrue(os.path.isfile(path))
+        self.transformation_config.name_link_unit("build/prg")
+
+    def test_each_pass_is_written_under_the_output_directory(self):
+        for pass_config in (self.analysis_config, self.transformation_config):
+            path = orchestrator.write_pass_config(self.workflow_config, 
pass_config)
+            self.assertEqual(os.path.join(self.tmp, pass_config.file_name), 
path)
+            self.assertTrue(os.path.isfile(path))
 
     def test_nothing_is_left_behind_by_the_write(self):
         orchestrator.write_pass_config(self.workflow_config, 
self.analysis_config)
         self.assertEqual(["config-analysis.json"], os.listdir(self.tmp))
 
+    def test_both_passes_can_be_written_and_coexist(self):
+        for pass_config in (self.analysis_config, self.transformation_config):
+            orchestrator.write_pass_config(self.workflow_config, pass_config)
+        self.assertEqual(
+            ["config-analysis.json", "config-transformation.json"],
+            sorted(os.listdir(self.tmp)),
+        )
+
     def test_the_document_does_not_carry_its_own_path(self):
         path = orchestrator.write_pass_config(
             self.workflow_config, self.analysis_config
@@ -156,16 +172,15 @@ def 
test_an_output_directory_that_does_not_exist_is_not_silently_skipped(self):
         with self.assertRaises(OSError):
             orchestrator.write_pass_config(workflow_config, 
self.analysis_config)
 
-    def test_a_shim_reads_back_the_run_and_its_own_pass(self):
-        path = orchestrator.write_pass_config(
-            self.workflow_config, self.analysis_config
-        )
-        workflow_config, pass_config = config_shims.bootstrap_from_env(
-            {config.SSAF_CONFIG_ENV_VAR: path}
-        )
-        self.assertEqual(self.workflow_config.to_json(), 
workflow_config.to_json())
-        self.assertIsInstance(pass_config, config.SSAFAnalysisPassConfig)
-        self.assertEqual("PointerFlow", pass_config.extractors)
+    def test_a_shim_reads_back_the_run_and_its_own_pass_whole(self):
+        for written in (self.analysis_config, self.transformation_config):
+            path = orchestrator.write_pass_config(self.workflow_config, 
written)
+            workflow_config, pass_config = config_shims.bootstrap_from_env(
+                {config.SSAF_CONFIG_ENV_VAR: path}
+            )
+            self.assertEqual(self.workflow_config.to_json(), 
workflow_config.to_json())
+            self.assertIs(type(written), type(pass_config))
+            self.assertEqual(written.to_json(), pass_config.to_json())
 
 
 class RunBuild(unittest.TestCase):
@@ -296,7 +311,7 @@ def _record(self, output_path, inputs):
             inputs,
         )
 
-    def _summary(self, build_artifact_path):
+    def _summary(self, build_artifact_path, document=None):
         path = utils.make_parents(
             self.ssaf_artifact_repo.get_path_for_artifact(
                 repository.get_ssaf_artifact_id(
@@ -305,16 +320,17 @@ def _summary(self, build_artifact_path):
                 )
             )
         )
-        document = {
-            "lu_namespace": [
-                {"kind": "LinkUnit", "name": 
os.path.basename(build_artifact_path)}
-            ]
-        }
+        if document is None:
+            document = {
+                "lu_namespace": [
+                    {"kind": "LinkUnit", "name": 
os.path.basename(build_artifact_path)}
+                ]
+            }
         with open(path, "w") as handle:
             json.dump(document, handle)
 
     def test_no_link_unit_at_all(self):
-        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+        link_unit, _, _, problem = orchestrator.get_topmost_link_unit(
             self.ssaf_artifact_repo, self.tmp
         )
         self.assertIsNone(link_unit)
@@ -323,7 +339,7 @@ def test_no_link_unit_at_all(self):
     def test_several_that_consume_none_of_each_other(self):
         self._record("/p/one", ["a.o"])
         self._record("/p/two", ["b.o"])
-        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+        link_unit, _, _, problem = orchestrator.get_topmost_link_unit(
             self.ssaf_artifact_repo, self.tmp
         )
         self.assertIsNone(link_unit)
@@ -333,21 +349,34 @@ def test_several_that_consume_none_of_each_other(self):
 
     def test_one_whose_summary_was_never_written(self):
         self._record("/p/prg", ["a.o"])
-        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+        link_unit, _, _, problem = orchestrator.get_topmost_link_unit(
             self.ssaf_artifact_repo, self.tmp
         )
         self.assertIsNone(link_unit)
         self.assertIn("no summary", problem.message)
         self.assertIn("p/prg", problem.message)
 
+    def test_one_whose_summary_names_no_link_unit(self):
+        self._record("/p/libx.a", ["a.o"])
+        self._summary(
+            "/p/libx.a", {"namespace": {"kind": "StaticLibrary", "name": 
"libx.a"}}
+        )
+        link_unit, _, _, problem = orchestrator.get_topmost_link_unit(
+            self.ssaf_artifact_repo, self.tmp
+        )
+        self.assertIsNone(link_unit)
+        self.assertIn("is not a link unit", problem.message)
+        self.assertIn("p/libx.a", problem.message)
+
     def test_one_with_its_summary_is_the_answer(self):
         self._record("/p/prg", ["a.o"])
         self._summary("/p/prg")
-        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+        link_unit, _, unit_id, problem = orchestrator.get_topmost_link_unit(
             self.ssaf_artifact_repo, self.tmp
         )
         self.assertIsNone(problem)
         self.assertEqual("/p/prg", link_unit.get_path())
+        self.assertEqual("prg", unit_id)
 
 
 if __name__ == "__main__":

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 f8b63c9b1acff..90280fb57d7eb 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
@@ -2,6 +2,7 @@
 # See https://llvm.org/LICENSE.txt for license information.
 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
 
+import json
 import os
 import shutil
 import tempfile
@@ -50,6 +51,42 @@ def test_artifact_paths(self):
                 )
             ),
         )
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/build/a.o.edits.yaml",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/build/a.o"),
+                    repository.SSAFArtifactKind.EDITS,
+                )
+            ),
+        )
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/build/a.o.report.sarif",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/build/a.o"),
+                    repository.SSAFArtifactKind.REPORT,
+                )
+            ),
+        )
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/prg.edits.yaml",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/prg"),
+                    repository.SSAFArtifactKind.EDITS,
+                )
+            ),
+        )
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/prg.conflicts.sarif",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/prg"),
+                    repository.SSAFArtifactKind.CONFLICTS,
+                )
+            ),
+        )
 
 
 class Construction(unittest.TestCase):
@@ -61,6 +98,89 @@ def test_constructing_it_creates_no_directory(self):
         self.assertFalse(os.path.exists(output_dir))
 
 
+class LinkUnitName(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.graph = configs.build_graph_for(self.tmp)
+
+    def _summary_path(self):
+        return self.ssaf_artifact_repo.get_path_for_artifact(
+            repository.get_ssaf_artifact_id(
+                build_graph.BuildArtifactId("/p/prg"),
+                repository.SSAFArtifactKind.SUMMARY,
+            )
+        )
+
+    def _write(self, document):
+        path = utils.make_parents(self._summary_path())
+        with open(path, "w") as handle:
+            json.dump(document, handle)
+
+    def _name(self):
+        return self.ssaf_artifact_repo.get_link_unit_name(self._summary_path())
+
+    def test_reads_the_link_unit_name(self):
+        self._write({"lu_namespace": [{"kind": "LinkUnit", "name": "prg"}]})
+        self.assertEqual("prg", self._name())
+
+    def test_a_library_names_no_link_unit(self):
+        self._write({"namespace": {"kind": "StaticLibrary", "name": "libx"}})
+        self.assertIsNone(self._name())
+
+    def test_skips_other_namespace_kinds(self):
+        self._write(
+            {
+                "lu_namespace": [
+                    {"kind": "StaticLibrary", "name": "libx"},
+                    {"kind": "LinkUnit", "name": "prg"},
+                ]
+            }
+        )
+        self.assertEqual("prg", self._name())
+
+    def test_absent_namespace(self):
+        self._write({})
+        self.assertIsNone(self._name())
+
+    def test_absent_summary(self):
+        self.assertIsNone(self._name())
+
+
+class Conflicts(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)
+
+    def _write(self, build_artifact_path, results):
+        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,
+                )
+            )
+        )
+        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())
+
+    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(),
+        )
+
+
 class Counts(unittest.TestCase):
     def setUp(self):
         self.tmp = tempfile.mkdtemp()
@@ -92,6 +212,14 @@ def test_each_kind_is_counted_apart_at_any_depth(self):
                 )
             )
         )
+        self._write(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/a.o"),
+                    repository.SSAFArtifactKind.EDITS,
+                )
+            )
+        )
         self._write(
             
self.graph.get_buildstep_record_path(build_graph.BuildArtifactId("/p/prg"))
         )


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

Reply via email to