[llvm-branch-commits] [clang] [llvm] [clang][ssaf] Add clang-ssaf-conflict-merge (PR #232547)

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


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

clang-ssaf-src-edit-merge reports the edits it drops because they
overlap, but only those of the one merge it ran. Add a tool that merges
such reports, so that the conflicts found while merging the edits of an
archive can be carried into the report of the program it is linked into.

This relands f32ac72b4f1e (#231731), reverted with its stack in
782610a701b4 (#232274), under a shorter name that matches
clang-ssaf-report-merge. The GN build files come from
1c563eafd44e (#232115).

Assisted-by: Claude



>From 4060e620a4eb0f57c25c63000d62194d86ac455e 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] Add clang-ssaf-conflict-merge

clang-ssaf-src-edit-merge reports the edits it drops because they
overlap, but only those of the one merge it ran. Add a tool that merges
such reports, so that the conflicts found while merging the edits of an
archive can be carried into the report of the program it is linked into.

This relands f32ac72b4f1e (#231731), reverted with its stack in
782610a701b4 (#232274), under a shorter name that matches
clang-ssaf-report-merge. The GN build files come from
1c563eafd44e (#232115).

Assisted-by: Claude
---
 .../user-docs/SourceEditGeneration.md         |  12 +
 .../Inputs/conflicts-a.sarif                  | 118 +++++++++
 .../Inputs/conflicts-b.sarif                  | 118 +++++++++
 .../Inputs/conflicts-c.sarif                  |  67 +++++
 .../Inputs/no-conflicts.sarif                 |  15 ++
 .../Inputs/other-tool.sarif                   |  15 ++
 .../ssaf-conflict-merge/Inputs/two-runs.sarif |  24 ++
 .../ssaf-conflict-merge/cli-errors.test       |  69 +++++
 .../Scalable/ssaf-conflict-merge/empty.test   |  14 +
 .../Scalable/ssaf-conflict-merge/help.test    |  19 ++
 .../Scalable/ssaf-conflict-merge/merge.test   |  28 ++
 .../Scalable/ssaf-conflict-merge/nested.test  |  17 ++
 .../ssaf-conflict-merge/order-invariance.test |   8 +
 .../src-edit-merge-reports.test               |  26 ++
 .../Scalable/ssaf-conflict-merge/version.test |   5 +
 clang/test/CMakeLists.txt                     |   1 +
 clang/tools/CMakeLists.txt                    |   1 +
 .../clang-ssaf-conflict-merge/CMakeLists.txt  |  12 +
 .../ConflictMerge.cpp                         | 247 ++++++++++++++++++
 llvm/utils/gn/secondary/clang/test/BUILD.gn   |   1 +
 .../tools/clang-ssaf-conflict-merge/BUILD.gn  |   8 +
 21 files changed, 825 insertions(+)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-a.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-b.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-c.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/no-conflicts.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/other-tool.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/two-runs.sarif
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/cli-errors.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/empty.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/help.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/merge.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/nested.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/order-invariance.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/src-edit-merge-reports.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-conflict-merge/version.test
 create mode 100644 clang/tools/clang-ssaf-conflict-merge/CMakeLists.txt
 create mode 100644 clang/tools/clang-ssaf-conflict-merge/ConflictMerge.cpp
 create mode 100644 llvm/utils/gn/secondary/clang/tools/clang-ssaf-conflict-merge/BUILD.gn

diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
index fb080d7d9d618..f50e5add31445 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
@@ -80,3 +80,15 @@ inputs share, such as one in a header that both translation units include,
 is reported once, and the result does not depend on the order of the
 inputs. A merged report can be merged again.
 
+## Merging conflict reports
+
+`clang-ssaf-src-edit-merge --sarif-conflicts-out=` reports the overlapping
+edits it dropped while merging. `clang-ssaf-conflict-merge` merges
+such reports the same way, so that the conflicts found while merging the
+edits of an archive reach the report of the program it is linked into:
+
+```console
+$ clang-ssaf-conflict-merge libparse.a.sarif prg.sarif -o all.sarif
+```
+
+The inputs must all have been written by the same tool.
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-a.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-a.sarif
new file mode 100644
index 0000000000000..110b144a6d266
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-a.sarif
@@ -0,0 +1,118 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteOffset": 10
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "2 overlapping replacement(s) at file:///src/x.cpp byte 10 were dropped; resolve manually."
+          },
+          "relatedLocations": [
+            {
+              "id": 1,
+              "message": {
+                "text": "candidate edit: \"X\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteLength": 5,
+                  "byteOffset": 10
+                }
+              }
+            },
+            {
+              "id": 2,
+              "message": {
+                "text": "candidate edit: \"Y\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteLength": 3,
+                  "byteOffset": 10
+                }
+              }
+            }
+          ],
+          "ruleId": "clang-reforge-replacement-conflict"
+        },
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteOffset": 4
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually."
+          },
+          "relatedLocations": [
+            {
+              "id": 1,
+              "message": {
+                "text": "candidate edit: \"int\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteLength": 3,
+                  "byteOffset": 4
+                }
+              }
+            },
+            {
+              "id": 2,
+              "message": {
+                "text": "candidate edit: \"long\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteLength": 4,
+                  "byteOffset": 4
+                }
+              }
+            }
+          ],
+          "ruleId": "clang-reforge-replacement-conflict"
+        }
+      ],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-b.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-b.sarif
new file mode 100644
index 0000000000000..7a7a92bb92921
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-b.sarif
@@ -0,0 +1,118 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteOffset": 4
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually."
+          },
+          "relatedLocations": [
+            {
+              "id": 1,
+              "message": {
+                "text": "candidate edit: \"int\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteLength": 3,
+                  "byteOffset": 4
+                }
+              }
+            },
+            {
+              "id": 2,
+              "message": {
+                "text": "candidate edit: \"long\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/shared.h"
+                },
+                "region": {
+                  "byteLength": 4,
+                  "byteOffset": 4
+                }
+              }
+            }
+          ],
+          "ruleId": "clang-reforge-replacement-conflict"
+        },
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/y.cpp"
+                },
+                "region": {
+                  "byteOffset": 30
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "2 overlapping replacement(s) at file:///src/y.cpp byte 30 were dropped; resolve manually."
+          },
+          "relatedLocations": [
+            {
+              "id": 1,
+              "message": {
+                "text": "candidate edit: \"Q\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/y.cpp"
+                },
+                "region": {
+                  "byteLength": 2,
+                  "byteOffset": 30
+                }
+              }
+            },
+            {
+              "id": 2,
+              "message": {
+                "text": "candidate edit: \"Z\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/y.cpp"
+                },
+                "region": {
+                  "byteLength": 2,
+                  "byteOffset": 30
+                }
+              }
+            }
+          ],
+          "ruleId": "clang-reforge-replacement-conflict"
+        }
+      ],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-c.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-c.sarif
new file mode 100644
index 0000000000000..dc5b1c020be8d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/conflicts-c.sarif
@@ -0,0 +1,67 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [
+        {
+          "level": "error",
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteOffset": 2
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "2 overlapping replacement(s) at file:///src/x.cpp byte 2 were dropped; resolve manually."
+          },
+          "relatedLocations": [
+            {
+              "id": 1,
+              "message": {
+                "text": "candidate edit: \"A\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteLength": 1,
+                  "byteOffset": 2
+                }
+              }
+            },
+            {
+              "id": 2,
+              "message": {
+                "text": "candidate edit: \"B\""
+              },
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "file:///src/x.cpp"
+                },
+                "region": {
+                  "byteLength": 1,
+                  "byteOffset": 2
+                }
+              }
+            }
+          ],
+          "ruleId": "clang-reforge-replacement-conflict"
+        }
+      ],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/no-conflicts.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/no-conflicts.sarif
new file mode 100644
index 0000000000000..da2d8c6941462
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/no-conflicts.sarif
@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/other-tool.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/other-tool.sarif
new file mode 100644
index 0000000000000..1c2ea28ea3ad4
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/other-tool.sarif
@@ -0,0 +1,15 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/two-runs.sarif b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/two-runs.sarif
new file mode 100644
index 0000000000000..cda78d1c4282d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/Inputs/two-runs.sarif
@@ -0,0 +1,24 @@
+{
+  "$schema": "https://json.schemastore.org/sarif-2.1.0.json",
+  "runs": [
+    {
+      "results": [],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    },
+    {
+      "results": [],
+      "tool": {
+        "driver": {
+          "name": "clang-ssaf-src-edit-merge",
+          "version": "24.0.0git"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/cli-errors.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/cli-errors.test
new file mode 100644
index 0000000000000..675cb5c42e5dd
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/cli-errors.test
@@ -0,0 +1,69 @@
+// Tests for clang-ssaf-conflict-merge errors.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// CLI error: no arguments
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-ARGS
+// NO-ARGS: clang-ssaf-conflict-merge{{(\.exe)?}}: Not enough positional command line arguments specified!
+
+// ============================================================================
+// CLI error: missing -o
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-OUTPUT
+// NO-OUTPUT: clang-ssaf-conflict-merge{{(\.exe)?}}: for the -o option: must be specified at least once!
+
+// ============================================================================
+// Error: nonexistent input
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge %t/missing.sarif \
+// RUN:   -o %t/out.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MISSING -DPATH=%t/missing.sarif
+// MISSING: clang-ssaf-conflict-merge: error: failed to read '[[PATH]]': {{.*}}
+
+// ============================================================================
+// Error: input is not JSON
+// ============================================================================
+
+// RUN: echo 'not json' > %t/not-json.sarif
+// RUN: not clang-ssaf-conflict-merge %t/not-json.sarif \
+// RUN:   -o %t/out.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NOT-JSON -DPATH=%t/not-json.sarif
+// NOT-JSON: clang-ssaf-conflict-merge: error: failed to parse '[[PATH]]': {{.*}}
+
+// ============================================================================
+// Error: input does not have exactly one run
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge %S/Inputs/two-runs.sarif \
+// RUN:   -o %t/out.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=TWO-RUNS
+// TWO-RUNS: clang-ssaf-conflict-merge: error: '{{.*}}two-runs.sarif' is not a SARIF log with exactly one run
+
+// ============================================================================
+// Error: inputs written by different tools
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/other-tool.sarif -o %t/out.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OTHER
+// OTHER: clang-ssaf-conflict-merge: error: '{{.*}}other-tool.sarif' was written by 'clang-ssaf', not by 'clang-ssaf-src-edit-merge'
+
+// ============================================================================
+// Error: the output cannot be written
+// ============================================================================
+
+// RUN: not clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   -o %t/no-such-dir/out.sarif 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=CANNOT-WRITE -DPATH=%t/no-such-dir/out.sarif
+// CANNOT-WRITE: clang-ssaf-conflict-merge: error: failed to write '[[PATH]]': {{.*}}
+
+// No error leaves an output behind.
+// RUN: not ls %t/out.sarif
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/empty.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/empty.test
new file mode 100644
index 0000000000000..dfa2d927a9020
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/empty.test
@@ -0,0 +1,14 @@
+// Reports without conflicts merge into a report without conflicts, and add
+// nothing to a report that has some.
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: clang-ssaf-conflict-merge %S/Inputs/no-conflicts.sarif \
+// RUN:   %S/Inputs/no-conflicts.sarif -o %t/none.sarif
+// RUN: FileCheck %s --input-file=%t/none.sarif
+// CHECK: "results": [],
+
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   -o %t/a.sarif
+// RUN: clang-ssaf-conflict-merge %S/Inputs/no-conflicts.sarif \
+// RUN:   %S/Inputs/conflicts-a.sarif -o %t/none-a.sarif
+// RUN: diff %t/a.sarif %t/none-a.sarif
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/help.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/help.test
new file mode 100644
index 0000000000000..de1262800c694
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/help.test
@@ -0,0 +1,19 @@
+// Test clang-ssaf-conflict-merge help option
+
+// RUN: clang-ssaf-conflict-merge --help-list-hidden \
+// RUN:           | FileCheck %s --match-full-lines
+
+// CHECK:      OVERVIEW: SSAF Conflict Report Merger
+// CHECK-EMPTY:
+// CHECK-NEXT: USAGE: clang-ssaf-conflict-merge{{(\.exe)?}} [options] <input.sarif>...
+// CHECK-EMPTY:
+// CHECK-NEXT: OPTIONS:
+// CHECK-NEXT:   -h                  - Alias for --help
+// CHECK-NEXT:   --help              - Display available options (--help-hidden for more)
+// CHECK-NEXT:   --help-hidden       - Display all available options
+// CHECK-NEXT:   --help-list         - Display list of available options (--help-list-hidden for more)
+// CHECK-NEXT:   --help-list-hidden  - Display list of all available options
+// CHECK-NEXT:   -o <path>           - Output file path
+// CHECK-NEXT:   --print-all-options - Print all option values after command line parsing
+// CHECK-NEXT:   --print-options     - Print non-default options after command line parsing
+// CHECK-NEXT:   --version           - Display the version of this program
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/merge.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/merge.test
new file mode 100644
index 0000000000000..c0823b5e51441
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/merge.test
@@ -0,0 +1,28 @@
+// Two conflict reports that share a conflict merge into one run.
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif -o %t/merged.sarif
+// RUN: FileCheck %s --input-file=%t/merged.sarif
+
+// '-' writes the report to stdout.
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif -o - > %t/stdout.sarif
+// RUN: diff %t/merged.sarif %t/stdout.sarif
+// RUN: grep '"text": "2 overlapping replacement(s) at file:///src/shared.h' \
+// RUN:   %t/merged.sarif | count 1
+
+// The conflicts in location order, the shared one once.
+// CHECK:      "results": [
+// CHECK:        "uri": "file:///src/shared.h"
+// CHECK:        "byteOffset": 4
+// CHECK:        "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually."
+// CHECK:        "text": "candidate edit: \"int\""
+// CHECK:        "text": "candidate edit: \"long\""
+// CHECK:        "uri": "file:///src/x.cpp"
+// CHECK:        "byteOffset": 10
+// CHECK:        "uri": "file:///src/y.cpp"
+// CHECK:        "byteOffset": 30
+// CHECK:      "tool": {
+// CHECK:        "name": "clang-ssaf-src-edit-merge",
+// CHECK:      "version": "2.1.0"
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/nested.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/nested.test
new file mode 100644
index 0000000000000..6a3fa47bdf819
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/nested.test
@@ -0,0 +1,17 @@
+// A merged report merges again like the reports it was made from, so that the
+// conflicts found while merging an archive's edits reach the report of the
+// program it is linked into.
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif -o %t/ab.sarif
+// RUN: clang-ssaf-conflict-merge %t/ab.sarif \
+// RUN:   %S/Inputs/conflicts-c.sarif -o %t/ab-c.sarif
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-c.sarif -o %t/abc.sarif
+// RUN: diff %t/ab-c.sarif %t/abc.sarif
+
+// Merging a report with itself adds nothing.
+// RUN: clang-ssaf-conflict-merge %t/ab.sarif %t/ab.sarif \
+// RUN:   -o %t/ab-ab.sarif
+// RUN: diff %t/ab.sarif %t/ab-ab.sarif
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/order-invariance.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/order-invariance.test
new file mode 100644
index 0000000000000..9cc4fff75f449
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/order-invariance.test
@@ -0,0 +1,8 @@
+// The merged report does not depend on the order of the inputs.
+
+// RUN: rm -rf %t && mkdir -p %t
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-a.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-c.sarif -o %t/abc.sarif
+// RUN: clang-ssaf-conflict-merge %S/Inputs/conflicts-c.sarif \
+// RUN:   %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-a.sarif -o %t/cba.sarif
+// RUN: diff %t/abc.sarif %t/cba.sarif
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/src-edit-merge-reports.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/src-edit-merge-reports.test
new file mode 100644
index 0000000000000..e7950fdd0e2bf
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/src-edit-merge-reports.test
@@ -0,0 +1,26 @@
+// Conflict reports written by clang-ssaf-src-edit-merge for two files merge
+// into one, and a report given twice contributes its conflicts once.
+
+// RUN: rm -rf %t.dir && mkdir -p %t.dir
+// RUN: printf '%%80s' '' > %t.dir/foo.cpp
+// RUN: printf '%%80s' '' > %t.dir/bar.cpp
+// DEFINE: %{inputs} = %S/../ssaf-src-edit-merge/Inputs
+// RUN: sed "s|@FOO@|%/t.dir/foo.cpp|g" %{inputs}/single-replacement.tpl.yaml > %t.dir/foo-a.yaml
+// RUN: sed "s|@FOO@|%/t.dir/foo.cpp|g" %{inputs}/conflict-text-y.tpl.yaml > %t.dir/foo-b.yaml
+// RUN: sed "s|@FOO@|%/t.dir/bar.cpp|g" %{inputs}/single-replacement.tpl.yaml > %t.dir/bar-a.yaml
+// RUN: sed "s|@FOO@|%/t.dir/bar.cpp|g" %{inputs}/conflict-text-z.tpl.yaml > %t.dir/bar-b.yaml
+// RUN: clang-ssaf-src-edit-merge %t.dir/foo-a.yaml %t.dir/foo-b.yaml \
+// RUN:   -o %t.dir/foo.yaml --sarif-conflicts-out=%t.dir/foo.sarif 2> /dev/null
+// RUN: clang-ssaf-src-edit-merge %t.dir/bar-a.yaml %t.dir/bar-b.yaml \
+// RUN:   -o %t.dir/bar.yaml --sarif-conflicts-out=%t.dir/bar.sarif 2> /dev/null
+
+// RUN: clang-ssaf-conflict-merge %t.dir/foo.sarif %t.dir/bar.sarif \
+// RUN:   %t.dir/foo.sarif -o %t.dir/merged.sarif
+// RUN: FileCheck %s --input-file=%t.dir/merged.sarif
+// RUN: grep '"ruleId": "clang-reforge-replacement-conflict"' %t.dir/merged.sarif \
+// RUN:   | count 2
+
+// CHECK:      "results": [
+// CHECK:        "uri": "file://{{.*}}bar.cpp"
+// CHECK:        "uri": "file://{{.*}}foo.cpp"
+// CHECK:        "name": "clang-ssaf-src-edit-merge",
diff --git a/clang/test/Analysis/Scalable/ssaf-conflict-merge/version.test b/clang/test/Analysis/Scalable/ssaf-conflict-merge/version.test
new file mode 100644
index 0000000000000..35e6da1f48fb0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-conflict-merge/version.test
@@ -0,0 +1,5 @@
+// Test clang-ssaf-conflict-merge version
+
+// RUN: clang-ssaf-conflict-merge --version | FileCheck %s
+
+// CHECK: clang-ssaf-conflict-merge 0.1
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index 943035a21f0af..e96711ffbe2cd 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -105,6 +105,7 @@ list(APPEND CLANG_TEST_DEPS
   clang-nvlink-wrapper
   clang-offload-bundler
   clang-ssaf-analyzer
+  clang-ssaf-conflict-merge
   clang-ssaf-format
   clang-ssaf-linker
   clang-ssaf-report-merge
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index bad63b18d9633..c7369b18c0639 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -16,6 +16,7 @@ add_clang_subdirectory(clang-nvlink-wrapper)
 add_clang_subdirectory(clang-offload-bundler)
 add_clang_subdirectory(clang-scan-deps)
 add_clang_subdirectory(clang-ssaf-analyzer)
+add_clang_subdirectory(clang-ssaf-conflict-merge)
 add_clang_subdirectory(clang-ssaf-format)
 add_clang_subdirectory(clang-ssaf-linker)
 if(NOT WIN32)
diff --git a/clang/tools/clang-ssaf-conflict-merge/CMakeLists.txt b/clang/tools/clang-ssaf-conflict-merge/CMakeLists.txt
new file mode 100644
index 0000000000000..ee75507a7564e
--- /dev/null
+++ b/clang/tools/clang-ssaf-conflict-merge/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(LLVM_LINK_COMPONENTS
+  Support
+  )
+
+add_clang_tool(clang-ssaf-conflict-merge
+  ConflictMerge.cpp
+  )
+
+clang_target_link_libraries(clang-ssaf-conflict-merge
+  PRIVATE
+  clangScalableStaticAnalysisTool
+  )
diff --git a/clang/tools/clang-ssaf-conflict-merge/ConflictMerge.cpp b/clang/tools/clang-ssaf-conflict-merge/ConflictMerge.cpp
new file mode 100644
index 0000000000000..87b51f25a19da
--- /dev/null
+++ b/clang/tools/clang-ssaf-conflict-merge/ConflictMerge.cpp
@@ -0,0 +1,247 @@
+//===- ConflictMerge.cpp - SSAF Conflict Report Merge Tool ----------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+//  This file implements the tool that merges SARIF conflict reports, as
+//  written by clang-ssaf-src-edit-merge --sarif-conflicts-out or by this tool,
+//  into one report. A conflict that several inputs report, such as one found
+//  while merging the edits of two archives that are linked into the same
+//  program, is kept once.
+//
+//===----------------------------------------------------------------------===//
+
+#include "clang/ScalableStaticAnalysis/Tool/Utils.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/InitLLVM.h"
+#include "llvm/Support/JSON.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/ToolOutputFile.h"
+#include "llvm/Support/raw_ostream.h"
+#include <cstdint>
+#include <set>
+#include <string>
+#include <tuple>
+#include <vector>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Command-Line Options
+//===----------------------------------------------------------------------===//
+
+cl::OptionCategory ConflictMergeCategory("clang-ssaf-conflict-merge options");
+
+cl::list<std::string> InputPaths(cl::Positional, cl::desc("<input.sarif>..."),
+                                 cl::OneOrMore, cl::cat(ConflictMergeCategory));
+
+cl::opt<std::string> OutputPath("o", cl::desc("Output file path"),
+                                cl::value_desc("path"), cl::Required,
+                                cl::cat(ConflictMergeCategory));
+
+//===----------------------------------------------------------------------===//
+// Error Messages
+//===----------------------------------------------------------------------===//
+
+namespace ErrorMessages {
+
+constexpr const char *CannotRead = "failed to read '{0}': {1}";
+
+constexpr const char *CannotParse = "failed to parse '{0}': {1}";
+
+constexpr const char *NotOneRun =
+    "'{0}' is not a SARIF log with exactly one run";
+
+constexpr const char *OtherTool = "'{0}' was written by '{1}', not by '{2}'";
+
+constexpr const char *CannotWrite = "failed to write '{0}': {1}";
+
+} // namespace ErrorMessages
+
+//===----------------------------------------------------------------------===//
+// Reading and Writing
+//===----------------------------------------------------------------------===//
+
+json::Value readLog(StringRef Path) {
+  ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer = MemoryBuffer::getFile(Path);
+  if (!Buffer) {
+    fail(ErrorMessages::CannotRead, Path, Buffer.getError().message());
+  }
+  Expected<json::Value> Log = json::parse((*Buffer)->getBuffer());
+  if (!Log) {
+    fail(ErrorMessages::CannotParse, Path, toString(Log.takeError()));
+  }
+  return std::move(*Log);
+}
+
+json::Object &getRun(json::Value &Log, StringRef Path) {
+  json::Object *Root = Log.getAsObject();
+  json::Array *Runs = Root ? Root->getArray("runs") : nullptr;
+  if (!Runs || Runs->size() != 1 || !Runs->front().getAsObject()) {
+    fail(ErrorMessages::NotOneRun, Path);
+  }
+  return *Runs->front().getAsObject();
+}
+
+void writeLog(const json::Value &Log, StringRef Path) {
+  std::error_code EC;
+  ToolOutputFile Out(Path, EC, sys::fs::OF_Text);
+  if (EC) {
+    fail(ErrorMessages::CannotWrite, Path, EC.message());
+  }
+  Out.os() << formatv("{0:2}", Log) << "\n";
+  Out.os().flush();
+  if (Out.os().has_error()) {
+    fail(ErrorMessages::CannotWrite, Path, Out.os().error().message());
+  }
+  Out.keep();
+}
+
+StringRef getString(const json::Object *Object, StringRef Key) {
+  if (!Object) {
+    return "";
+  }
+  return Object->getString(Key).value_or("");
+}
+
+/// Returns a copy of \p Object without the members named in \p Excluded.
+json::Object copyExcept(const json::Object &Object,
+                        ArrayRef<StringRef> Excluded) {
+  json::Object Copy;
+  for (const auto &[Key, Value] : Object) {
+    if (!is_contained(Excluded, StringRef(Key))) {
+      Copy[Key] = Value;
+    }
+  }
+  return Copy;
+}
+
+//===----------------------------------------------------------------------===//
+// Merging
+//===----------------------------------------------------------------------===//
+
+struct Conflict {
+  std::string Uri;
+  int64_t Offset;
+  std::string Message;
+  std::string Serialized;
+  json::Value Result = nullptr;
+
+  bool operator<(const Conflict &Other) const {
+    return std::tie(Uri, Offset, Message, Serialized) <
+           std::tie(Other.Uri, Other.Offset, Other.Message, Other.Serialized);
+  }
+};
+
+class ConflictReportMerger {
+public:
+  void add(json::Value Log, StringRef Path);
+  json::Value takeMerged();
+
+private:
+  void addConflict(json::Value Result);
+
+  bool HasInput = false;
+  json::Object Root;
+  json::Object Run;
+  std::set<std::string> Seen;
+  std::vector<Conflict> Conflicts;
+};
+
+void ConflictReportMerger::add(json::Value Log, StringRef Path) {
+  json::Object &InputRun = getRun(Log, Path);
+  const json::Object *InputTool = InputRun.getObject("tool");
+  StringRef InputName =
+      getString(InputTool ? InputTool->getObject("driver") : nullptr, "name");
+
+  if (!HasInput) {
+    HasInput = true;
+    Root = copyExcept(*Log.getAsObject(), {"runs"});
+    Run = copyExcept(InputRun, {"results"});
+  } else {
+    const json::Object *Tool = Run.getObject("tool");
+    StringRef Name =
+        getString(Tool ? Tool->getObject("driver") : nullptr, "name");
+    if (InputName != Name) {
+      fail(ErrorMessages::OtherTool, Path, InputName, Name);
+    }
+  }
+
+  if (json::Array *Results = InputRun.getArray("results")) {
+    for (json::Value &Result : *Results) {
+      addConflict(std::move(Result));
+    }
+  }
+}
+
+void ConflictReportMerger::addConflict(json::Value Result) {
+  Conflict C;
+  C.Serialized = formatv("{0}", Result);
+  if (!Seen.insert(C.Serialized).second) {
+    return;
+  }
+  C.Offset = 0;
+  const json::Object *Object = Result.getAsObject();
+  const json::Array *Locations =
+      Object ? Object->getArray("locations") : nullptr;
+  if (Locations && !Locations->empty()) {
+    const json::Object *Location = Locations->front().getAsObject();
+    const json::Object *Physical =
+        Location ? Location->getObject("physicalLocation") : nullptr;
+    if (Physical) {
+      C.Uri = getString(Physical->getObject("artifactLocation"), "uri").str();
+      if (const json::Object *Region = Physical->getObject("region")) {
+        C.Offset = Region->getInteger("byteOffset").value_or(0);
+      }
+    }
+  }
+  C.Message =
+      getString(Object ? Object->getObject("message") : nullptr, "text").str();
+  C.Result = std::move(Result);
+  Conflicts.push_back(std::move(C));
+}
+
+json::Value ConflictReportMerger::takeMerged() {
+  llvm::sort(Conflicts);
+  json::Array Results;
+  for (Conflict &C : Conflicts) {
+    Results.push_back(std::move(C.Result));
+  }
+  Run["results"] = std::move(Results);
+  json::Array Runs;
+  Runs.push_back(std::move(Run));
+  Root["runs"] = std::move(Runs);
+  return std::move(Root);
+}
+
+} // namespace
+
+//===----------------------------------------------------------------------===//
+// Driver
+//===----------------------------------------------------------------------===//
+
+int main(int argc, const char **argv) {
+  llvm::StringRef ToolHeading = "SSAF Conflict Report Merger";
+
+  InitLLVM X(argc, argv);
+  initTool(argc, argv, "0.1", ConflictMergeCategory, ToolHeading);
+
+  ConflictReportMerger Merger;
+  for (const std::string &Path : InputPaths) {
+    Merger.add(readLog(Path), Path);
+  }
+  writeLog(Merger.takeMerged(), OutputPath);
+
+  return 0;
+}
diff --git a/llvm/utils/gn/secondary/clang/test/BUILD.gn b/llvm/utils/gn/secondary/clang/test/BUILD.gn
index 80b7c88dcf2d1..e4cb35528cf8b 100644
--- a/llvm/utils/gn/secondary/clang/test/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/test/BUILD.gn
@@ -165,6 +165,7 @@ group("test") {
     "//clang/tools/clang-repl",
     "//clang/tools/clang-scan-deps",
     "//clang/tools/clang-ssaf-analyzer",
+    "//clang/tools/clang-ssaf-conflict-merge",
     "//clang/tools/clang-ssaf-format",
     "//clang/tools/clang-ssaf-linker",
     "//clang/tools/clang-ssaf-orchestrator-make",
diff --git a/llvm/utils/gn/secondary/clang/tools/clang-ssaf-conflict-merge/BUILD.gn b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-conflict-merge/BUILD.gn
new file mode 100644
index 0000000000000..e4da626c03ed3
--- /dev/null
+++ b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-conflict-merge/BUILD.gn
@@ -0,0 +1,8 @@
+executable("clang-ssaf-conflict-merge") {
+  configs += [ "//llvm/utils/gn/build:clang_code" ]
+  deps = [
+    "//clang/lib/ScalableStaticAnalysis/Tool",
+    "//llvm/lib/Support",
+  ]
+  sources = [ "ConflictMerge.cpp" ]
+}



More information about the llvm-branch-commits mailing list