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 <[email protected]>
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;
+}

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

Reply via email to