[clang] [clang][ssaf] Add clang-ssaf-merge-conflict-report (PR #231731)
via cfe-commits
cfe-commits at lists.llvm.org
Tue Oct 13 20:07:36 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-static-analyzer-1
Author: Jan Korous (jkorous-apple)
<details>
<summary>Changes</summary>
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.
---
Patch is 33.76 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/231731.diff
19 Files Affected:
- (modified) clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md (+12)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif (+118)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif (+118)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif (+67)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif (+15)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif (+15)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif (+24)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test (+69)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test (+14)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test (+19)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test (+28)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test (+17)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test (+8)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test (+26)
- (added) clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test (+5)
- (modified) clang/test/CMakeLists.txt (+1)
- (modified) clang/tools/CMakeLists.txt (+1)
- (added) clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt (+12)
- (added) clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp (+249)
``````````diff
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-m...
[truncated]
``````````
</details>
https://github.com/llvm/llvm-project/pull/231731
More information about the cfe-commits
mailing list