[clang] [clang][ssaf] Add clang-ssaf-merge-conflict-report (PR #231731)
Jan Korous via cfe-commits
cfe-commits at lists.llvm.org
Tue Oct 13 20:06:58 PDT 2026
https://github.com/jkorous-apple created https://github.com/llvm/llvm-project/pull/231731
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.
>From e5223a0bbb204cc121ef53100ada3a2b10c71ebe Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Mon, 12 Oct 2026 23:02:56 -0700
Subject: [PATCH] [clang][ssaf] Add clang-ssaf-merge-conflict-report
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.
---
.../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 ++
.../Inputs/two-runs.sarif | 24 ++
.../cli-errors.test | 69 +++++
.../ssaf-merge-conflict-report/empty.test | 14 +
.../ssaf-merge-conflict-report/help.test | 19 ++
.../ssaf-merge-conflict-report/merge.test | 28 ++
.../ssaf-merge-conflict-report/nested.test | 17 ++
.../order-invariance.test | 8 +
.../src-edit-merge-reports.test | 26 ++
.../ssaf-merge-conflict-report/version.test | 5 +
clang/test/CMakeLists.txt | 1 +
clang/tools/CMakeLists.txt | 1 +
.../CMakeLists.txt | 12 +
.../MergeConflictReport.cpp | 249 ++++++++++++++++++
19 files changed, 818 insertions(+)
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test
create mode 100644 clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt
create mode 100644 clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp
diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
index a0b52b3f7df38..842238730a106 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
@@ -81,3 +81,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-merge-conflict-report` 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-merge-conflict-report 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-merge-conflict-report/Inputs/conflicts-a.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif
new file mode 100644
index 0000000000000..110b144a6d266
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/Inputs/conflicts-b.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif
new file mode 100644
index 0000000000000..7a7a92bb92921
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/Inputs/conflicts-c.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif
new file mode 100644
index 0000000000000..dc5b1c020be8d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/Inputs/no-conflicts.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif
new file mode 100644
index 0000000000000..da2d8c6941462
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/Inputs/other-tool.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif
new file mode 100644
index 0000000000000..1c2ea28ea3ad4
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/Inputs/two-runs.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif
new file mode 100644
index 0000000000000..cda78d1c4282d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report/cli-errors.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test
new file mode 100644
index 0000000000000..05a17c2f6cfab
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test
@@ -0,0 +1,69 @@
+// Tests for clang-ssaf-merge-conflict-report errors.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// CLI error: no arguments
+// ============================================================================
+
+// RUN: not clang-ssaf-merge-conflict-report 2>&1 \
+// RUN: | FileCheck %s --check-prefix=NO-ARGS
+// NO-ARGS: clang-ssaf-merge-conflict-report{{(\.exe)?}}: Not enough positional command line arguments specified!
+
+// ============================================================================
+// CLI error: missing -o
+// ============================================================================
+
+// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif 2>&1 \
+// RUN: | FileCheck %s --check-prefix=NO-OUTPUT
+// NO-OUTPUT: clang-ssaf-merge-conflict-report{{(\.exe)?}}: for the -o option: must be specified at least once!
+
+// ============================================================================
+// Error: nonexistent input
+// ============================================================================
+
+// RUN: not clang-ssaf-merge-conflict-report %t/missing.sarif \
+// RUN: -o %t/out.sarif 2>&1 \
+// RUN: | FileCheck %s --check-prefix=MISSING -DPATH=%t/missing.sarif
+// MISSING: clang-ssaf-merge-conflict-report: error: failed to read '[[PATH]]': {{.*}}
+
+// ============================================================================
+// Error: input is not JSON
+// ============================================================================
+
+// RUN: echo 'not json' > %t/not-json.sarif
+// RUN: not clang-ssaf-merge-conflict-report %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-merge-conflict-report: error: failed to parse '[[PATH]]': {{.*}}
+
+// ============================================================================
+// Error: input does not have exactly one run
+// ============================================================================
+
+// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/two-runs.sarif \
+// RUN: -o %t/out.sarif 2>&1 \
+// RUN: | FileCheck %s --check-prefix=TWO-RUNS
+// TWO-RUNS: clang-ssaf-merge-conflict-report: error: '{{.*}}two-runs.sarif' is not a SARIF log with exactly one run
+
+// ============================================================================
+// Error: inputs written by different tools
+// ============================================================================
+
+// RUN: not clang-ssaf-merge-conflict-report %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-merge-conflict-report: 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-merge-conflict-report %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-merge-conflict-report: 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-merge-conflict-report/empty.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test
new file mode 100644
index 0000000000000..948cdfd95cedd
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report %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-merge-conflict-report %S/Inputs/conflicts-a.sarif \
+// RUN: -o %t/a.sarif
+// RUN: clang-ssaf-merge-conflict-report %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-merge-conflict-report/help.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test
new file mode 100644
index 0000000000000..c8f0884255f9c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test
@@ -0,0 +1,19 @@
+// Test clang-ssaf-merge-conflict-report help option
+
+// RUN: clang-ssaf-merge-conflict-report --help-list-hidden \
+// RUN: | FileCheck %s --match-full-lines
+
+// CHECK: OVERVIEW: SSAF Conflict Report Merger
+// CHECK-EMPTY:
+// CHECK-NEXT: USAGE: clang-ssaf-merge-conflict-report{{(\.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-merge-conflict-report/merge.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test
new file mode 100644
index 0000000000000..e54317ff9b809
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report %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-merge-conflict-report %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-merge-conflict-report/nested.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test
new file mode 100644
index 0000000000000..7f7bce20c8dac
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report %S/Inputs/conflicts-a.sarif \
+// RUN: %S/Inputs/conflicts-b.sarif -o %t/ab.sarif
+// RUN: clang-ssaf-merge-conflict-report %t/ab.sarif \
+// RUN: %S/Inputs/conflicts-c.sarif -o %t/ab-c.sarif
+// RUN: clang-ssaf-merge-conflict-report %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-merge-conflict-report %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-merge-conflict-report/order-invariance.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test
new file mode 100644
index 0000000000000..18845ac4c56bd
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report %S/Inputs/conflicts-a.sarif \
+// RUN: %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-c.sarif -o %t/abc.sarif
+// RUN: clang-ssaf-merge-conflict-report %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-merge-conflict-report/src-edit-merge-reports.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test
new file mode 100644
index 0000000000000..2581255bb5469
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/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-merge-conflict-report %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-merge-conflict-report/version.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test
new file mode 100644
index 0000000000000..6a0d6babd8967
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test
@@ -0,0 +1,5 @@
+// Test clang-ssaf-merge-conflict-report version
+
+// RUN: clang-ssaf-merge-conflict-report --version | FileCheck %s
+
+// CHECK: clang-ssaf-merge-conflict-report 0.1
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index 793bed1fbc0a3..e498ef13f71e2 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -107,6 +107,7 @@ list(APPEND CLANG_TEST_DEPS
clang-ssaf-analyzer
clang-ssaf-format
clang-ssaf-linker
+ clang-ssaf-merge-conflict-report
clang-ssaf-merge-transformation-report
clang-ssaf-src-edit-merge
clang-sycl-linker
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index c38efad58db16..f85381e65ac20 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -18,6 +18,7 @@ add_clang_subdirectory(clang-scan-deps)
add_clang_subdirectory(clang-ssaf-analyzer)
add_clang_subdirectory(clang-ssaf-format)
add_clang_subdirectory(clang-ssaf-linker)
+add_clang_subdirectory(clang-ssaf-merge-conflict-report)
add_clang_subdirectory(clang-ssaf-merge-transformation-report)
if(NOT WIN32)
add_clang_subdirectory(clang-ssaf-orchestrator-make)
diff --git a/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt b/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt
new file mode 100644
index 0000000000000..1dfdba8dce21e
--- /dev/null
+++ b/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt
@@ -0,0 +1,12 @@
+set(LLVM_LINK_COMPONENTS
+ Support
+ )
+
+add_clang_tool(clang-ssaf-merge-conflict-report
+ MergeConflictReport.cpp
+ )
+
+clang_target_link_libraries(clang-ssaf-merge-conflict-report
+ PRIVATE
+ clangScalableStaticAnalysisTool
+ )
diff --git a/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp b/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp
new file mode 100644
index 0000000000000..6d4aa84cf3651
--- /dev/null
+++ b/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp
@@ -0,0 +1,249 @@
+//===- MergeConflictReport.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
+ MergeConflictReportCategory("clang-ssaf-merge-conflict-report options");
+
+cl::list<std::string> InputPaths(cl::Positional, cl::desc("<input.sarif>..."),
+ cl::OneOrMore,
+ cl::cat(MergeConflictReportCategory));
+
+cl::opt<std::string> OutputPath("o", cl::desc("Output file path"),
+ cl::value_desc("path"), cl::Required,
+ cl::cat(MergeConflictReportCategory));
+
+//===----------------------------------------------------------------------===//
+// 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", MergeConflictReportCategory, ToolHeading);
+
+ ConflictReportMerger Merger;
+ for (const std::string &Path : InputPaths) {
+ Merger.add(readLog(Path), Path);
+ }
+ writeLog(Merger.takeMerged(), OutputPath);
+
+ return 0;
+}
More information about the cfe-commits
mailing list