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
