https://github.com/jkorous-apple created https://github.com/llvm/llvm-project/pull/231731
clang-ssaf-src-edit-merge reports the edits it drops because they overlap, but only those of the one merge it ran. Add a tool that merges such reports, so that the conflicts found while merging the edits of an archive can be carried into the report of the program it is linked into. >From e5223a0bbb204cc121ef53100ada3a2b10c71ebe Mon Sep 17 00:00:00 2001 From: Jan Korous <[email protected]> Date: Mon, 12 Oct 2026 23:02:56 -0700 Subject: [PATCH] [clang][ssaf] Add clang-ssaf-merge-conflict-report clang-ssaf-src-edit-merge reports the edits it drops because they overlap, but only those of the one merge it ran. Add a tool that merges such reports, so that the conflicts found while merging the edits of an archive can be carried into the report of the program it is linked into. --- .../user-docs/SourceEditGeneration.md | 12 + .../Inputs/conflicts-a.sarif | 118 +++++++++ .../Inputs/conflicts-b.sarif | 118 +++++++++ .../Inputs/conflicts-c.sarif | 67 +++++ .../Inputs/no-conflicts.sarif | 15 ++ .../Inputs/other-tool.sarif | 15 ++ .../Inputs/two-runs.sarif | 24 ++ .../cli-errors.test | 69 +++++ .../ssaf-merge-conflict-report/empty.test | 14 + .../ssaf-merge-conflict-report/help.test | 19 ++ .../ssaf-merge-conflict-report/merge.test | 28 ++ .../ssaf-merge-conflict-report/nested.test | 17 ++ .../order-invariance.test | 8 + .../src-edit-merge-reports.test | 26 ++ .../ssaf-merge-conflict-report/version.test | 5 + clang/test/CMakeLists.txt | 1 + clang/tools/CMakeLists.txt | 1 + .../CMakeLists.txt | 12 + .../MergeConflictReport.cpp | 249 ++++++++++++++++++ 19 files changed, 818 insertions(+) create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test create mode 100644 clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test create mode 100644 clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt create mode 100644 clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md index a0b52b3f7df38..842238730a106 100644 --- a/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md +++ b/clang/docs/ScalableStaticAnalysis/user-docs/SourceEditGeneration.md @@ -81,3 +81,15 @@ inputs share, such as one in a header that both translation units include, is reported once, and the result does not depend on the order of the inputs. A merged report can be merged again. +## Merging conflict reports + +`clang-ssaf-src-edit-merge --sarif-conflicts-out=` reports the overlapping +edits it dropped while merging. `clang-ssaf-merge-conflict-report` merges +such reports the same way, so that the conflicts found while merging the +edits of an archive reach the report of the program it is linked into: + +```console +$ clang-ssaf-merge-conflict-report libparse.a.sarif prg.sarif -o all.sarif +``` + +The inputs must all have been written by the same tool. diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif new file mode 100644 index 0000000000000..110b144a6d266 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-a.sarif @@ -0,0 +1,118 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteOffset": 10 + } + } + } + ], + "message": { + "text": "2 overlapping replacement(s) at file:///src/x.cpp byte 10 were dropped; resolve manually." + }, + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "candidate edit: \"X\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteLength": 5, + "byteOffset": 10 + } + } + }, + { + "id": 2, + "message": { + "text": "candidate edit: \"Y\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteLength": 3, + "byteOffset": 10 + } + } + } + ], + "ruleId": "clang-reforge-replacement-conflict" + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteOffset": 4 + } + } + } + ], + "message": { + "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually." + }, + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "candidate edit: \"int\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteLength": 3, + "byteOffset": 4 + } + } + }, + { + "id": 2, + "message": { + "text": "candidate edit: \"long\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteLength": 4, + "byteOffset": 4 + } + } + } + ], + "ruleId": "clang-reforge-replacement-conflict" + } + ], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif new file mode 100644 index 0000000000000..7a7a92bb92921 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-b.sarif @@ -0,0 +1,118 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteOffset": 4 + } + } + } + ], + "message": { + "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually." + }, + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "candidate edit: \"int\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteLength": 3, + "byteOffset": 4 + } + } + }, + { + "id": 2, + "message": { + "text": "candidate edit: \"long\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/shared.h" + }, + "region": { + "byteLength": 4, + "byteOffset": 4 + } + } + } + ], + "ruleId": "clang-reforge-replacement-conflict" + }, + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/y.cpp" + }, + "region": { + "byteOffset": 30 + } + } + } + ], + "message": { + "text": "2 overlapping replacement(s) at file:///src/y.cpp byte 30 were dropped; resolve manually." + }, + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "candidate edit: \"Q\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/y.cpp" + }, + "region": { + "byteLength": 2, + "byteOffset": 30 + } + } + }, + { + "id": 2, + "message": { + "text": "candidate edit: \"Z\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/y.cpp" + }, + "region": { + "byteLength": 2, + "byteOffset": 30 + } + } + } + ], + "ruleId": "clang-reforge-replacement-conflict" + } + ], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif new file mode 100644 index 0000000000000..dc5b1c020be8d --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/conflicts-c.sarif @@ -0,0 +1,67 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [ + { + "level": "error", + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteOffset": 2 + } + } + } + ], + "message": { + "text": "2 overlapping replacement(s) at file:///src/x.cpp byte 2 were dropped; resolve manually." + }, + "relatedLocations": [ + { + "id": 1, + "message": { + "text": "candidate edit: \"A\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteLength": 1, + "byteOffset": 2 + } + } + }, + { + "id": 2, + "message": { + "text": "candidate edit: \"B\"" + }, + "physicalLocation": { + "artifactLocation": { + "uri": "file:///src/x.cpp" + }, + "region": { + "byteLength": 1, + "byteOffset": 2 + } + } + } + ], + "ruleId": "clang-reforge-replacement-conflict" + } + ], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif new file mode 100644 index 0000000000000..da2d8c6941462 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/no-conflicts.sarif @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif new file mode 100644 index 0000000000000..1c2ea28ea3ad4 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/other-tool.sarif @@ -0,0 +1,15 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [], + "tool": { + "driver": { + "name": "clang-ssaf", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif new file mode 100644 index 0000000000000..cda78d1c4282d --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/Inputs/two-runs.sarif @@ -0,0 +1,24 @@ +{ + "$schema": "https://json.schemastore.org/sarif-2.1.0.json", + "runs": [ + { + "results": [], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + }, + { + "results": [], + "tool": { + "driver": { + "name": "clang-ssaf-src-edit-merge", + "version": "24.0.0git" + } + } + } + ], + "version": "2.1.0" +} diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test new file mode 100644 index 0000000000000..05a17c2f6cfab --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/cli-errors.test @@ -0,0 +1,69 @@ +// Tests for clang-ssaf-merge-conflict-report errors. + +// RUN: rm -rf %t +// RUN: mkdir -p %t + +// ============================================================================ +// CLI error: no arguments +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report 2>&1 \ +// RUN: | FileCheck %s --check-prefix=NO-ARGS +// NO-ARGS: clang-ssaf-merge-conflict-report{{(\.exe)?}}: Not enough positional command line arguments specified! + +// ============================================================================ +// CLI error: missing -o +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=NO-OUTPUT +// NO-OUTPUT: clang-ssaf-merge-conflict-report{{(\.exe)?}}: for the -o option: must be specified at least once! + +// ============================================================================ +// Error: nonexistent input +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report %t/missing.sarif \ +// RUN: -o %t/out.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=MISSING -DPATH=%t/missing.sarif +// MISSING: clang-ssaf-merge-conflict-report: error: failed to read '[[PATH]]': {{.*}} + +// ============================================================================ +// Error: input is not JSON +// ============================================================================ + +// RUN: echo 'not json' > %t/not-json.sarif +// RUN: not clang-ssaf-merge-conflict-report %t/not-json.sarif \ +// RUN: -o %t/out.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=NOT-JSON -DPATH=%t/not-json.sarif +// NOT-JSON: clang-ssaf-merge-conflict-report: error: failed to parse '[[PATH]]': {{.*}} + +// ============================================================================ +// Error: input does not have exactly one run +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/two-runs.sarif \ +// RUN: -o %t/out.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=TWO-RUNS +// TWO-RUNS: clang-ssaf-merge-conflict-report: error: '{{.*}}two-runs.sarif' is not a SARIF log with exactly one run + +// ============================================================================ +// Error: inputs written by different tools +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/other-tool.sarif -o %t/out.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=OTHER +// OTHER: clang-ssaf-merge-conflict-report: error: '{{.*}}other-tool.sarif' was written by 'clang-ssaf', not by 'clang-ssaf-src-edit-merge' + +// ============================================================================ +// Error: the output cannot be written +// ============================================================================ + +// RUN: not clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: -o %t/no-such-dir/out.sarif 2>&1 \ +// RUN: | FileCheck %s --check-prefix=CANNOT-WRITE -DPATH=%t/no-such-dir/out.sarif +// CANNOT-WRITE: clang-ssaf-merge-conflict-report: error: failed to write '[[PATH]]': {{.*}} + +// No error leaves an output behind. +// RUN: not ls %t/out.sarif diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test new file mode 100644 index 0000000000000..948cdfd95cedd --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/empty.test @@ -0,0 +1,14 @@ +// Reports without conflicts merge into a report without conflicts, and add +// nothing to a report that has some. + +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/no-conflicts.sarif \ +// RUN: %S/Inputs/no-conflicts.sarif -o %t/none.sarif +// RUN: FileCheck %s --input-file=%t/none.sarif +// CHECK: "results": [], + +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: -o %t/a.sarif +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/no-conflicts.sarif \ +// RUN: %S/Inputs/conflicts-a.sarif -o %t/none-a.sarif +// RUN: diff %t/a.sarif %t/none-a.sarif diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test new file mode 100644 index 0000000000000..c8f0884255f9c --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/help.test @@ -0,0 +1,19 @@ +// Test clang-ssaf-merge-conflict-report help option + +// RUN: clang-ssaf-merge-conflict-report --help-list-hidden \ +// RUN: | FileCheck %s --match-full-lines + +// CHECK: OVERVIEW: SSAF Conflict Report Merger +// CHECK-EMPTY: +// CHECK-NEXT: USAGE: clang-ssaf-merge-conflict-report{{(\.exe)?}} [options] <input.sarif>... +// CHECK-EMPTY: +// CHECK-NEXT: OPTIONS: +// CHECK-NEXT: -h - Alias for --help +// CHECK-NEXT: --help - Display available options (--help-hidden for more) +// CHECK-NEXT: --help-hidden - Display all available options +// CHECK-NEXT: --help-list - Display list of available options (--help-list-hidden for more) +// CHECK-NEXT: --help-list-hidden - Display list of all available options +// CHECK-NEXT: -o <path> - Output file path +// CHECK-NEXT: --print-all-options - Print all option values after command line parsing +// CHECK-NEXT: --print-options - Print non-default options after command line parsing +// CHECK-NEXT: --version - Display the version of this program diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test new file mode 100644 index 0000000000000..e54317ff9b809 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/merge.test @@ -0,0 +1,28 @@ +// Two conflict reports that share a conflict merge into one run. + +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif -o %t/merged.sarif +// RUN: FileCheck %s --input-file=%t/merged.sarif + +// '-' writes the report to stdout. +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif -o - > %t/stdout.sarif +// RUN: diff %t/merged.sarif %t/stdout.sarif +// RUN: grep '"text": "2 overlapping replacement(s) at file:///src/shared.h' \ +// RUN: %t/merged.sarif | count 1 + +// The conflicts in location order, the shared one once. +// CHECK: "results": [ +// CHECK: "uri": "file:///src/shared.h" +// CHECK: "byteOffset": 4 +// CHECK: "text": "2 overlapping replacement(s) at file:///src/shared.h byte 4 were dropped; resolve manually." +// CHECK: "text": "candidate edit: \"int\"" +// CHECK: "text": "candidate edit: \"long\"" +// CHECK: "uri": "file:///src/x.cpp" +// CHECK: "byteOffset": 10 +// CHECK: "uri": "file:///src/y.cpp" +// CHECK: "byteOffset": 30 +// CHECK: "tool": { +// CHECK: "name": "clang-ssaf-src-edit-merge", +// CHECK: "version": "2.1.0" diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test new file mode 100644 index 0000000000000..7f7bce20c8dac --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/nested.test @@ -0,0 +1,17 @@ +// A merged report merges again like the reports it was made from, so that the +// conflicts found while merging an archive's edits reach the report of the +// program it is linked into. + +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif -o %t/ab.sarif +// RUN: clang-ssaf-merge-conflict-report %t/ab.sarif \ +// RUN: %S/Inputs/conflicts-c.sarif -o %t/ab-c.sarif +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-c.sarif -o %t/abc.sarif +// RUN: diff %t/ab-c.sarif %t/abc.sarif + +// Merging a report with itself adds nothing. +// RUN: clang-ssaf-merge-conflict-report %t/ab.sarif %t/ab.sarif \ +// RUN: -o %t/ab-ab.sarif +// RUN: diff %t/ab.sarif %t/ab-ab.sarif diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test new file mode 100644 index 0000000000000..18845ac4c56bd --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/order-invariance.test @@ -0,0 +1,8 @@ +// The merged report does not depend on the order of the inputs. + +// RUN: rm -rf %t && mkdir -p %t +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-a.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-c.sarif -o %t/abc.sarif +// RUN: clang-ssaf-merge-conflict-report %S/Inputs/conflicts-c.sarif \ +// RUN: %S/Inputs/conflicts-b.sarif %S/Inputs/conflicts-a.sarif -o %t/cba.sarif +// RUN: diff %t/abc.sarif %t/cba.sarif diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test new file mode 100644 index 0000000000000..2581255bb5469 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/src-edit-merge-reports.test @@ -0,0 +1,26 @@ +// Conflict reports written by clang-ssaf-src-edit-merge for two files merge +// into one, and a report given twice contributes its conflicts once. + +// RUN: rm -rf %t.dir && mkdir -p %t.dir +// RUN: printf '%%80s' '' > %t.dir/foo.cpp +// RUN: printf '%%80s' '' > %t.dir/bar.cpp +// DEFINE: %{inputs} = %S/../ssaf-src-edit-merge/Inputs +// RUN: sed "s|@FOO@|%/t.dir/foo.cpp|g" %{inputs}/single-replacement.tpl.yaml > %t.dir/foo-a.yaml +// RUN: sed "s|@FOO@|%/t.dir/foo.cpp|g" %{inputs}/conflict-text-y.tpl.yaml > %t.dir/foo-b.yaml +// RUN: sed "s|@FOO@|%/t.dir/bar.cpp|g" %{inputs}/single-replacement.tpl.yaml > %t.dir/bar-a.yaml +// RUN: sed "s|@FOO@|%/t.dir/bar.cpp|g" %{inputs}/conflict-text-z.tpl.yaml > %t.dir/bar-b.yaml +// RUN: clang-ssaf-src-edit-merge %t.dir/foo-a.yaml %t.dir/foo-b.yaml \ +// RUN: -o %t.dir/foo.yaml --sarif-conflicts-out=%t.dir/foo.sarif 2> /dev/null +// RUN: clang-ssaf-src-edit-merge %t.dir/bar-a.yaml %t.dir/bar-b.yaml \ +// RUN: -o %t.dir/bar.yaml --sarif-conflicts-out=%t.dir/bar.sarif 2> /dev/null + +// RUN: clang-ssaf-merge-conflict-report %t.dir/foo.sarif %t.dir/bar.sarif \ +// RUN: %t.dir/foo.sarif -o %t.dir/merged.sarif +// RUN: FileCheck %s --input-file=%t.dir/merged.sarif +// RUN: grep '"ruleId": "clang-reforge-replacement-conflict"' %t.dir/merged.sarif \ +// RUN: | count 2 + +// CHECK: "results": [ +// CHECK: "uri": "file://{{.*}}bar.cpp" +// CHECK: "uri": "file://{{.*}}foo.cpp" +// CHECK: "name": "clang-ssaf-src-edit-merge", diff --git a/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test new file mode 100644 index 0000000000000..6a0d6babd8967 --- /dev/null +++ b/clang/test/Analysis/Scalable/ssaf-merge-conflict-report/version.test @@ -0,0 +1,5 @@ +// Test clang-ssaf-merge-conflict-report version + +// RUN: clang-ssaf-merge-conflict-report --version | FileCheck %s + +// CHECK: clang-ssaf-merge-conflict-report 0.1 diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt index 793bed1fbc0a3..e498ef13f71e2 100644 --- a/clang/test/CMakeLists.txt +++ b/clang/test/CMakeLists.txt @@ -107,6 +107,7 @@ list(APPEND CLANG_TEST_DEPS clang-ssaf-analyzer clang-ssaf-format clang-ssaf-linker + clang-ssaf-merge-conflict-report clang-ssaf-merge-transformation-report clang-ssaf-src-edit-merge clang-sycl-linker diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt index c38efad58db16..f85381e65ac20 100644 --- a/clang/tools/CMakeLists.txt +++ b/clang/tools/CMakeLists.txt @@ -18,6 +18,7 @@ add_clang_subdirectory(clang-scan-deps) add_clang_subdirectory(clang-ssaf-analyzer) add_clang_subdirectory(clang-ssaf-format) add_clang_subdirectory(clang-ssaf-linker) +add_clang_subdirectory(clang-ssaf-merge-conflict-report) add_clang_subdirectory(clang-ssaf-merge-transformation-report) if(NOT WIN32) add_clang_subdirectory(clang-ssaf-orchestrator-make) diff --git a/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt b/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt new file mode 100644 index 0000000000000..1dfdba8dce21e --- /dev/null +++ b/clang/tools/clang-ssaf-merge-conflict-report/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LLVM_LINK_COMPONENTS + Support + ) + +add_clang_tool(clang-ssaf-merge-conflict-report + MergeConflictReport.cpp + ) + +clang_target_link_libraries(clang-ssaf-merge-conflict-report + PRIVATE + clangScalableStaticAnalysisTool + ) diff --git a/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp b/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp new file mode 100644 index 0000000000000..6d4aa84cf3651 --- /dev/null +++ b/clang/tools/clang-ssaf-merge-conflict-report/MergeConflictReport.cpp @@ -0,0 +1,249 @@ +//===- MergeConflictReport.cpp - SSAF Conflict Report Merge Tool ----------===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// +// +// This file implements the tool that merges SARIF conflict reports, as +// written by clang-ssaf-src-edit-merge --sarif-conflicts-out or by this tool, +// into one report. A conflict that several inputs report, such as one found +// while merging the edits of two archives that are linked into the same +// program, is kept once. +// +//===----------------------------------------------------------------------===// + +#include "clang/ScalableStaticAnalysis/Tool/Utils.h" +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/FileSystem.h" +#include "llvm/Support/FormatVariadic.h" +#include "llvm/Support/InitLLVM.h" +#include "llvm/Support/JSON.h" +#include "llvm/Support/MemoryBuffer.h" +#include "llvm/Support/ToolOutputFile.h" +#include "llvm/Support/raw_ostream.h" +#include <cstdint> +#include <set> +#include <string> +#include <tuple> +#include <vector> + +using namespace llvm; +using namespace clang::ssaf; + +namespace { + +//===----------------------------------------------------------------------===// +// Command-Line Options +//===----------------------------------------------------------------------===// + +cl::OptionCategory + MergeConflictReportCategory("clang-ssaf-merge-conflict-report options"); + +cl::list<std::string> InputPaths(cl::Positional, cl::desc("<input.sarif>..."), + cl::OneOrMore, + cl::cat(MergeConflictReportCategory)); + +cl::opt<std::string> OutputPath("o", cl::desc("Output file path"), + cl::value_desc("path"), cl::Required, + cl::cat(MergeConflictReportCategory)); + +//===----------------------------------------------------------------------===// +// Error Messages +//===----------------------------------------------------------------------===// + +namespace ErrorMessages { + +constexpr const char *CannotRead = "failed to read '{0}': {1}"; + +constexpr const char *CannotParse = "failed to parse '{0}': {1}"; + +constexpr const char *NotOneRun = + "'{0}' is not a SARIF log with exactly one run"; + +constexpr const char *OtherTool = "'{0}' was written by '{1}', not by '{2}'"; + +constexpr const char *CannotWrite = "failed to write '{0}': {1}"; + +} // namespace ErrorMessages + +//===----------------------------------------------------------------------===// +// Reading and Writing +//===----------------------------------------------------------------------===// + +json::Value readLog(StringRef Path) { + ErrorOr<std::unique_ptr<MemoryBuffer>> Buffer = MemoryBuffer::getFile(Path); + if (!Buffer) { + fail(ErrorMessages::CannotRead, Path, Buffer.getError().message()); + } + Expected<json::Value> Log = json::parse((*Buffer)->getBuffer()); + if (!Log) { + fail(ErrorMessages::CannotParse, Path, toString(Log.takeError())); + } + return std::move(*Log); +} + +json::Object &getRun(json::Value &Log, StringRef Path) { + json::Object *Root = Log.getAsObject(); + json::Array *Runs = Root ? Root->getArray("runs") : nullptr; + if (!Runs || Runs->size() != 1 || !Runs->front().getAsObject()) { + fail(ErrorMessages::NotOneRun, Path); + } + return *Runs->front().getAsObject(); +} + +void writeLog(const json::Value &Log, StringRef Path) { + std::error_code EC; + ToolOutputFile Out(Path, EC, sys::fs::OF_Text); + if (EC) { + fail(ErrorMessages::CannotWrite, Path, EC.message()); + } + Out.os() << formatv("{0:2}", Log) << "\n"; + Out.os().flush(); + if (Out.os().has_error()) { + fail(ErrorMessages::CannotWrite, Path, Out.os().error().message()); + } + Out.keep(); +} + +StringRef getString(const json::Object *Object, StringRef Key) { + if (!Object) { + return ""; + } + return Object->getString(Key).value_or(""); +} + +/// Returns a copy of \p Object without the members named in \p Excluded. +json::Object copyExcept(const json::Object &Object, + ArrayRef<StringRef> Excluded) { + json::Object Copy; + for (const auto &[Key, Value] : Object) { + if (!is_contained(Excluded, StringRef(Key))) { + Copy[Key] = Value; + } + } + return Copy; +} + +//===----------------------------------------------------------------------===// +// Merging +//===----------------------------------------------------------------------===// + +struct Conflict { + std::string Uri; + int64_t Offset; + std::string Message; + std::string Serialized; + json::Value Result = nullptr; + + bool operator<(const Conflict &Other) const { + return std::tie(Uri, Offset, Message, Serialized) < + std::tie(Other.Uri, Other.Offset, Other.Message, Other.Serialized); + } +}; + +class ConflictReportMerger { +public: + void add(json::Value Log, StringRef Path); + json::Value takeMerged(); + +private: + void addConflict(json::Value Result); + + bool HasInput = false; + json::Object Root; + json::Object Run; + std::set<std::string> Seen; + std::vector<Conflict> Conflicts; +}; + +void ConflictReportMerger::add(json::Value Log, StringRef Path) { + json::Object &InputRun = getRun(Log, Path); + const json::Object *InputTool = InputRun.getObject("tool"); + StringRef InputName = + getString(InputTool ? InputTool->getObject("driver") : nullptr, "name"); + + if (!HasInput) { + HasInput = true; + Root = copyExcept(*Log.getAsObject(), {"runs"}); + Run = copyExcept(InputRun, {"results"}); + } else { + const json::Object *Tool = Run.getObject("tool"); + StringRef Name = + getString(Tool ? Tool->getObject("driver") : nullptr, "name"); + if (InputName != Name) { + fail(ErrorMessages::OtherTool, Path, InputName, Name); + } + } + + if (json::Array *Results = InputRun.getArray("results")) { + for (json::Value &Result : *Results) { + addConflict(std::move(Result)); + } + } +} + +void ConflictReportMerger::addConflict(json::Value Result) { + Conflict C; + C.Serialized = formatv("{0}", Result); + if (!Seen.insert(C.Serialized).second) { + return; + } + C.Offset = 0; + const json::Object *Object = Result.getAsObject(); + const json::Array *Locations = + Object ? Object->getArray("locations") : nullptr; + if (Locations && !Locations->empty()) { + const json::Object *Location = Locations->front().getAsObject(); + const json::Object *Physical = + Location ? Location->getObject("physicalLocation") : nullptr; + if (Physical) { + C.Uri = getString(Physical->getObject("artifactLocation"), "uri").str(); + if (const json::Object *Region = Physical->getObject("region")) { + C.Offset = Region->getInteger("byteOffset").value_or(0); + } + } + } + C.Message = + getString(Object ? Object->getObject("message") : nullptr, "text").str(); + C.Result = std::move(Result); + Conflicts.push_back(std::move(C)); +} + +json::Value ConflictReportMerger::takeMerged() { + llvm::sort(Conflicts); + json::Array Results; + for (Conflict &C : Conflicts) { + Results.push_back(std::move(C.Result)); + } + Run["results"] = std::move(Results); + json::Array Runs; + Runs.push_back(std::move(Run)); + Root["runs"] = std::move(Runs); + return std::move(Root); +} + +} // namespace + +//===----------------------------------------------------------------------===// +// Driver +//===----------------------------------------------------------------------===// + +int main(int argc, const char **argv) { + llvm::StringRef ToolHeading = "SSAF Conflict Report Merger"; + + InitLLVM X(argc, argv); + initTool(argc, argv, "0.1", MergeConflictReportCategory, ToolHeading); + + ConflictReportMerger Merger; + for (const std::string &Path : InputPaths) { + Merger.add(readLog(Path), Path); + } + writeLog(Merger.takeMerged(), OutputPath); + + return 0; +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
