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

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-src-edit-merge. The GN build files come from
1c563eafd44e (#232115).

Assisted-by: Claude




>From 5aee35742ceb241d3c27db7da4ccec800a2bbf5a Mon Sep 17 00:00:00 2001
From: Jan Korous <[email protected]>
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-src-edit-merge. The GN build files come from
1c563eafd44e (#232115).

Assisted-by: Claude
---
 .../user-docs/SourceEditGeneration.md         |  13 +
 .../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, 826 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 af7fc7f56aa8ca..bb580c1bdc2042 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md
@@ -65,3 +65,16 @@ $ clang-apply-replacements --remove-change-desc-files 
<dir-with-yaml>
 
 The transformation report can be consumed by any SARIF viewer.
 
+
+## 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, 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 00000000000000..110b144a6d266b
--- /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 00000000000000..7a7a92bb92921e
--- /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 00000000000000..dc5b1c020be8d5
--- /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 00000000000000..da2d8c69414628
--- /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 00000000000000..1c2ea28ea3ad42
--- /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 00000000000000..cda78d1c4282dc
--- /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 00000000000000..675cb5c42e5dd3
--- /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 00000000000000..dfa2d927a9020b
--- /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 00000000000000..de1262800c6944
--- /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 00000000000000..c0823b5e514410
--- /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 00000000000000..6a3fa47bdf8195
--- /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 00000000000000..9cc4fff75f4493
--- /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 00000000000000..e7950fdd0e2bfa
--- /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 00000000000000..35e6da1f48fb02
--- /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 3cbc8354900fa6..5125ac8745733d 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-src-edit-merge
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index 855af780d4e02a..05366ac22ab855 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)
 add_clang_subdirectory(clang-ssaf-src-edit-merge)
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 00000000000000..ee75507a7564ed
--- /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 00000000000000..87b51f25a19da3
--- /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 84486497551873..0aecfae4b07156 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-src-edit-merge",
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 00000000000000..e4da626c03ed3c
--- /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" ]
+}

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

Reply via email to