Author: Rashmi Mudduluru Date: 2026-08-27T12:00:38-07:00 New Revision: a6bf0bd01b60df2544b051615c330a4b9b18f359
URL: https://github.com/llvm/llvm-project/commit/a6bf0bd01b60df2544b051615c330a4b9b18f359 DIFF: https://github.com/llvm/llvm-project/commit/a6bf0bd01b60df2544b051615c330a4b9b18f359.diff LOG: [clang][ssaf][clang-reforge] Tool for merging per-TU source-edit files (#216183) Add a new tool that merges per-TU clang::tooling::TranslationUnitReplacements YAML files for one link unit into a single merged YAML, deduplicating identical edits and dropping conflicting overlapping edits rather than silently picking a winner. The tool does not apply edits to source files; that remains the caller's responsibility via clang-apply-replacements. rdar://179151250 Added: clang-tools-extra/clang-ssaf-src-edit-merge/CMakeLists.txt clang-tools-extra/clang-ssaf-src-edit-merge/SrcEditMerge.cpp clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-q-offset-30.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-y.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-z.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/empty.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/internal-conflict.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-a.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-b.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-cluster-of-three.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-distinct-offsets.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-non-zero-length-replacement.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement-offset-30.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-zero-length-insert.tpl.yaml clang-tools-extra/test/clang-ssaf-src-edit-merge/bad-args.test clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-preserves-unrelated-zero-length-insert.test clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-stderr-without-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/empty-input.test clang-tools-extra/test/clang-ssaf-src-edit-merge/help.test clang-tools-extra/test/clang-ssaf-src-edit-merge/identical-overlap.test clang-tools-extra/test/clang-ssaf-src-edit-merge/lit.local.cfg clang-tools-extra/test/clang-ssaf-src-edit-merge/malformed-yaml.test clang-tools-extra/test/clang-ssaf-src-edit-merge/no-conflict-sarif-empty.test clang-tools-extra/test/clang-ssaf-src-edit-merge/non-overlapping.test clang-tools-extra/test/clang-ssaf-src-edit-merge/order-invariance.test clang-tools-extra/test/clang-ssaf-src-edit-merge/parent-dir-missing.test clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-identical-non-zero-length.test clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-zero-length-insert.test clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-cluster-of-three-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-conflict-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/sarif-out-parent-dir-missing.test clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input-internal-conflict.test clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input.test clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-all-conflict.test clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-conflict-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/two-conflicts-different-offsets-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict-sarif.test clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict.test Modified: clang-tools-extra/CMakeLists.txt clang-tools-extra/test/CMakeLists.txt Removed: ################################################################################ diff --git a/clang-tools-extra/CMakeLists.txt b/clang-tools-extra/CMakeLists.txt index 87050db4e0e75..0dbdeaed9a9a2 100644 --- a/clang-tools-extra/CMakeLists.txt +++ b/clang-tools-extra/CMakeLists.txt @@ -27,6 +27,7 @@ add_subdirectory(clang-doc) add_subdirectory(clang-include-fixer) add_subdirectory(clang-move) add_subdirectory(clang-query) +add_subdirectory(clang-ssaf-src-edit-merge) add_subdirectory(include-cleaner) add_subdirectory(pp-trace) add_subdirectory(tool-template) diff --git a/clang-tools-extra/clang-ssaf-src-edit-merge/CMakeLists.txt b/clang-tools-extra/clang-ssaf-src-edit-merge/CMakeLists.txt new file mode 100644 index 0000000000000..a9045b9fd3f90 --- /dev/null +++ b/clang-tools-extra/clang-ssaf-src-edit-merge/CMakeLists.txt @@ -0,0 +1,12 @@ +set(LLVM_LINK_COMPONENTS + Support + ) + +add_clang_tool(clang-ssaf-src-edit-merge + SrcEditMerge.cpp + ) + +clang_target_link_libraries(clang-ssaf-src-edit-merge + PRIVATE + clangToolingCore + ) diff --git a/clang-tools-extra/clang-ssaf-src-edit-merge/SrcEditMerge.cpp b/clang-tools-extra/clang-ssaf-src-edit-merge/SrcEditMerge.cpp new file mode 100644 index 0000000000000..d66fc657f594b --- /dev/null +++ b/clang-tools-extra/clang-ssaf-src-edit-merge/SrcEditMerge.cpp @@ -0,0 +1,539 @@ +//===- SrcEditMerge.cpp ---------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +// +// clang-ssaf-src-edit-merge: per-LU source-edit YAML merge tool. +// +// Reads N per-TU clang::tooling::TranslationUnitReplacements YAML files, +// deduplicates and merges them into one flat, conflict-resolved list (see +// "Conflict policy" below), and writes a single merged YAML spanning all N +// TUs. The tool does NOT write source files — applying the merge result is +// the caller's responsibility (typically clang-reforge invokes +// `clang-apply-replacements` after this tool returns). +// +// Conflict policy: this tool implements a drop-all policy. For each file, a +// maximal group of transitively-overlapping input Replacements (a cluster) +// is computed directly from the input; if a cluster has more than one +// member, every member is removed from the merged output — not just enough +// of them to resolve the overlap. A one-line stderr summary is emitted per +// dropped cluster, and the tool still exits 0. Any input file that does not +// exist on disk is excluded entirely from the merged output. +// +//===----------------------------------------------------------------------===// + +#include "clang/Basic/Version.h" +#include "clang/Tooling/ReplacementsYaml.h" // IWYU pragma: keep +#include "llvm/ADT/ArrayRef.h" +#include "llvm/ADT/STLExtras.h" +#include "llvm/ADT/SmallString.h" +#include "llvm/ADT/StringRef.h" +#include "llvm/Support/CommandLine.h" +#include "llvm/Support/Error.h" +#include "llvm/Support/ErrorOr.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/Path.h" +#include "llvm/Support/YAMLTraits.h" +#include "llvm/Support/raw_ostream.h" + +#include <algorithm> +#include <map> +#include <set> +#include <string> +#include <utility> +#include <vector> + +namespace { + +namespace cl = llvm::cl; + +//===----------------------------------------------------------------------===// +// Error Messages +//===----------------------------------------------------------------------===// + +constexpr const char *ToolName = "clang-ssaf-src-edit-merge"; + +constexpr const char *CannotReadInput = "cannot read {0}: {1}"; + +constexpr const char *InvalidReplacementsYaml = + "{0}: invalid TranslationUnitReplacements YAML"; + +constexpr const char *ConflictClusterSummary = + "conflict: skipped {0} overlapping replacement(s) at {1}:{2}"; + +constexpr const char *CannotWriteFile = "cannot write {0}"; + +constexpr const char *WriteErrorOnFile = "write error on {0}"; + +constexpr const char *CannotWriteOutput = "cannot write {0}: {1}"; + +constexpr const char *MissingReplacementFile = + "{0}: file does not exist; skipping its replacement(s)"; + +constexpr const char *CandidateEditMessage = "candidate edit: \"{0}\""; + +constexpr const char *ConflictSarifMessage = + "{0} overlapping replacement(s) at {1} byte {2} were dropped; resolve " + "manually."; + +cl::OptionCategory MergeCategory("clang-ssaf-src-edit-merge options"); + +cl::list<std::string> InputFiles(cl::Positional, cl::OneOrMore, + cl::desc("<input.yaml>..."), + cl::cat(MergeCategory)); + +cl::opt<std::string> OutputFile("o", cl::Required, cl::value_desc("path"), + cl::desc("Output path for the merged YAML."), + cl::cat(MergeCategory)); + +cl::opt<std::string> SarifConflictsOut( + "sarif-conflicts-out", cl::value_desc("path"), + cl::desc("Optional path. When supplied, write a SARIF document " + "listing conflict clusters dropped from the merged output."), + cl::cat(MergeCategory)); + +/// Read one input YAML into a TranslationUnitReplacements. +/// +/// Returns true on success. On failure, prints a one-line diagnostic to +/// stderr and returns false; the caller surfaces this as a non-zero exit. +bool readInput(llvm::StringRef Path, + clang::tooling::TranslationUnitReplacements &Out) { + llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> Buffer = + llvm::MemoryBuffer::getFile(Path); + if (std::error_code EC = Buffer.getError()) { + llvm::errs() << ToolName << ": " + << llvm::formatv(CannotReadInput, Path, EC.message()) << "\n"; + return false; + } + llvm::yaml::Input YAML(Buffer.get()->getBuffer()); + YAML >> Out; + if (YAML.error()) { + llvm::errs() << ToolName << ": " + << llvm::formatv(InvalidReplacementsYaml, Path) << "\n"; + return false; + } + return true; +} + +/// The merged output: a flat list of Replacements spanning every input file. +/// +/// Deliberately not clang::tooling::TranslationUnitReplacements: that type +/// represents one TU's replacements, but this represents the drop-all-policy +/// result of merging N TUs' worth of edits. `MappingTraits` below serializes +/// this to the identical YAML shape (the same two required keys, +/// MainSourceFile and Replacements) so clang-apply-replacements — which only +/// knows how to read that shape — can still consume the output; only the +/// C++ type stops making a claim about the data it doesn't hold. +/// MainSourceFile is populated on a best-effort basis (computeMainSourceFile, +/// below) purely for wire compatibility: nothing in the merge/apply pipeline +/// reads it back. +struct MergedReplacements { + std::string MainSourceFile; + std::vector<clang::tooling::Replacement> Replacements; +}; + +/// Compute the shared MainSourceFile across inputs. +/// +/// Per spec: if every input declares the same MainSourceFile, use that; +/// otherwise use the empty string. +std::string computeMainSourceFile( + const std::vector<clang::tooling::TranslationUnitReplacements> &TUs) { + if (TUs.empty()) + return ""; + const std::string &First = TUs.front().MainSourceFile; + for (const auto &TU : TUs) + if (TU.MainSourceFile != First) + return ""; + return First; +} + +/// Build the conflict cluster list from the merged input key set. +/// +/// `InputKeysByFile` is every input Replacement with length > 0, grouped by +/// file. Zero-length insertions are excluded by the caller because they never +/// overlap anything and are a no-op to apply, so they can never affect this +/// function's clustering. +/// +/// A cluster is a maximal connected component of one file's input +/// replacements whose [offset, offset+length) byte ranges transitively +/// overlap. +/// +/// The walk merges into the current cluster whenever +/// key.offset < lastEnd, where +/// lastEnd = max(member.offset + member.length) across cluster members. +/// Otherwise the current cluster closes and a new one opens. +/// +/// Only clusters of size > 1 are returned; singletons are not conflicts. +/// +/// Each returned cluster's member list is sorted by (offset, length, text); +/// the cluster list itself is sorted by (file, min-offset) ascending. This +/// pins iteration order for both stderr cluster lines and (in a follow-on +/// task) the SARIF results array. +std::vector<std::vector<clang::tooling::Replacement>> buildConflictClusters( + const std::map<std::string, std::set<clang::tooling::Replacement>> + &InputKeysByFile) { + std::vector<std::vector<clang::tooling::Replacement>> Clusters; + for (auto &Entry : InputKeysByFile) { + // Keys is a std::set<Replacement>, so it's already ordered by + // Replacement::operator< — the (offset, length, text) order the cluster + // walk below needs, since every entry here shares Entry.first as its + // file path. + auto &Keys = Entry.second; + + std::vector<clang::tooling::Replacement> Current; + unsigned LastEnd = 0; + auto Flush = [&]() { + if (Current.size() > 1) + Clusters.push_back(std::move(Current)); + Current.clear(); + LastEnd = 0; + }; + + for (const clang::tooling::Replacement &K : Keys) { + if (Current.empty()) { + Current.push_back(K); + LastEnd = K.getOffset() + K.getLength(); + continue; + } + if (K.getOffset() < LastEnd) { + Current.push_back(K); + LastEnd = std::max(LastEnd, K.getOffset() + K.getLength()); + } else { + Flush(); + Current.push_back(K); + LastEnd = K.getOffset() + K.getLength(); + } + } + Flush(); + } + + // Pin cluster-list order by (file, min-offset) ascending. + llvm::sort(Clusters, [](const std::vector<clang::tooling::Replacement> &A, + const std::vector<clang::tooling::Replacement> &B) { + if (A.front().getFilePath() != B.front().getFilePath()) + return A.front().getFilePath() < B.front().getFilePath(); + return A.front().getOffset() < B.front().getOffset(); + }); + + return Clusters; +} + +/// Emit one stderr line per conflict cluster. +/// +/// Precondition: `Clusters` is sorted by (file, min-offset) ascending — +/// guaranteed by buildConflictClusters, the only place Clusters is built. +void emitConflictClusterLines( + const std::vector<std::vector<clang::tooling::Replacement>> &Clusters) { + for (const auto &Cluster : Clusters) { + llvm::errs() << llvm::formatv(ConflictClusterSummary, Cluster.size(), + Cluster.front().getFilePath(), + Cluster.front().getOffset()) + << "\n"; + } +} + +/// Canonicalize a Replacement's `FilePath` into an absolute `file://` URI. +/// +/// Fallback chain: +/// 1. `llvm::sys::fs::real_path` — resolves symlinks and yields an +/// absolute path. Only succeeds if the file exists on disk. +/// 2. `llvm::sys::fs::make_absolute` — succeeds for non-existent paths +/// too; used for synthetic test fixtures whose FilePath may name a +/// file that the merger never opened. +/// 3. Raw `FilePath` — last-resort fallback if both of the above fail. +/// Emits a syntactically valid `file://` URI even if the underlying +/// path is relative, matching the SARIF requirement's "absolute" +/// promise loosely (downstream tooling that needs strict absolute +/// URIs SHOULD canonicalize on its end if the disk state permits). +std::string canonicalizeToFileUri(llvm::StringRef FilePath) { + llvm::SmallString<256> Buf; + if (!llvm::sys::fs::real_path(FilePath, Buf)) + return "file://" + llvm::sys::path::convert_to_slash(Buf); + Buf.assign(FilePath.begin(), FilePath.end()); + if (!llvm::sys::fs::make_absolute(Buf)) + return "file://" + llvm::sys::path::convert_to_slash(Buf); + return "file://" + llvm::sys::path::convert_to_slash(FilePath); +} + +/// Emit a SARIF document at `Path` listing every conflict cluster. +/// +/// `Clusters` SHALL be pre-sorted by `(file, min-offset)` ascending by the +/// caller; this emitter walks them in order to populate +/// `runs[0].results[]`. Within each cluster, `relatedLocations[]` is +/// sorted locally by `(byteLength, candidate-text)` ascending per the +/// "SARIF conflict report" requirement. +/// +/// Even when `Clusters` is empty, this writes a well-formed SARIF +/// document with `runs[0].results: []`. The file's presence is the +/// "merger ran with conflict reporting requested" signal. +llvm::Error emitConflictSarif( + llvm::StringRef Path, + llvm::ArrayRef<std::vector<clang::tooling::Replacement>> Clusters) { + llvm::json::Array Results; + Results.reserve(Clusters.size()); + + for (const auto &Cluster : Clusters) { + const clang::tooling::Replacement &Min = Cluster.front(); + std::string Uri = canonicalizeToFileUri(Min.getFilePath()); + + // Re-sort cluster members locally by (byteLength, text) ascending. + std::vector<clang::tooling::Replacement> Sorted(Cluster.begin(), + Cluster.end()); + llvm::sort(Sorted, [](const clang::tooling::Replacement &A, + const clang::tooling::Replacement &B) { + if (A.getLength() != B.getLength()) + return A.getLength() < B.getLength(); + return A.getReplacementText() < B.getReplacementText(); + }); + + llvm::json::Array RelatedLocations; + RelatedLocations.reserve(Sorted.size()); + for (size_t I = 0; I < Sorted.size(); ++I) { + const clang::tooling::Replacement &K = Sorted[I]; + RelatedLocations.push_back(llvm::json::Object{ + {"id", static_cast<int64_t>(I + 1)}, + {"physicalLocation", + llvm::json::Object{ + {"artifactLocation", llvm::json::Object{{"uri", Uri}}}, + {"region", + llvm::json::Object{ + {"byteOffset", static_cast<int64_t>(K.getOffset())}, + {"byteLength", static_cast<int64_t>(K.getLength())}}}}}, + {"message", + llvm::json::Object{{"text", llvm::formatv(CandidateEditMessage, + K.getReplacementText()) + .str()}}}}); + } + + std::string MessageText = + llvm::formatv(ConflictSarifMessage, Cluster.size(), Uri, + Min.getOffset()) + .str(); + + Results.push_back(llvm::json::Object{ + {"ruleId", "clang-reforge-replacement-conflict"}, + {"level", "error"}, + {"message", llvm::json::Object{{"text", MessageText}}}, + {"locations", + llvm::json::Array{llvm::json::Object{ + {"physicalLocation", + llvm::json::Object{ + {"artifactLocation", llvm::json::Object{{"uri", Uri}}}, + {"region", llvm::json::Object{{"byteOffset", + static_cast<int64_t>( + Min.getOffset())}}}}}}}}, + {"relatedLocations", std::move(RelatedLocations)}}); + } + + llvm::json::Value Doc = llvm::json::Object{ + {"version", "2.1.0"}, + {"$schema", "https://json.schemastore.org/sarif-2.1.0.json"}, + {"runs", + llvm::json::Array{llvm::json::Object{ + {"tool", + llvm::json::Object{ + {"driver", + llvm::json::Object{{"name", ToolName}, + {"version", CLANG_VERSION_STRING}}}}}, + {"results", std::move(Results)}}}}}; + + std::error_code EC; + llvm::raw_fd_ostream OS(Path, EC, llvm::sys::fs::OF_Text); + if (EC) + return llvm::createStringError(EC, + llvm::formatv(CannotWriteFile, Path).str()); + // Pretty-print with indent 2 via the json::Value format_provider. + OS << llvm::formatv("{0:2}", Doc) << "\n"; + OS.flush(); + if (OS.has_error()) + return llvm::createStringError(OS.error(), + llvm::formatv(WriteErrorOnFile, Path).str()); + return llvm::Error::success(); +} + +/// Returns whether `Path`'s parent directory exists, so a bad `-o` or +/// `--sarif-conflicts-out` path can be rejected before any merge work runs. +/// A `Path` with no directory component (e.g. a bare file name) is treated +/// as valid — it names a file in the current directory. This is a +/// best-effort check, not a substitute for handling the real open() failure: +/// it cannot catch permission errors or a race between the check and the +/// eventual write. +bool parentDirectoryExists(llvm::StringRef Path) { + llvm::StringRef Parent = llvm::sys::path::parent_path(Path); + return Parent.empty() || llvm::sys::fs::is_directory(Parent); +} + +} // namespace + +namespace llvm { +namespace yaml { +/// Specialized MappingTraits to describe how a MergedReplacements is +/// (de)serialized. Mirrors MappingTraits<TranslationUnitReplacements> in +/// ReplacementsYaml.h exactly, key-for-key, for wire compatibility. +template <> struct MappingTraits<MergedReplacements> { + static void mapping(IO &Io, MergedReplacements &Doc) { + Io.mapRequired("MainSourceFile", Doc.MainSourceFile); + Io.mapRequired("Replacements", Doc.Replacements); + } +}; +} // namespace yaml +} // namespace llvm + +int main(int argc, const char **argv) { + llvm::InitLLVM X(argc, argv); + cl::HideUnrelatedOptions(MergeCategory); + cl::ParseCommandLineOptions( + argc, argv, + "clang-ssaf-src-edit-merge: merge per-TU TranslationUnitReplacements " + "YAML files for one link unit into a single merged YAML. Does not " + "write source files; the apply step is the caller's responsibility.\n"); + + // Validate the command-line parameters that can be checked without + // reading any input, so a bad -o or --sarif-conflicts-out path is rejected + // before the (potentially expensive) merge work below runs. + if (!parentDirectoryExists(OutputFile)) { + llvm::errs() << ToolName << ": " + << llvm::formatv(CannotWriteFile, OutputFile) << "\n"; + return 1; + } + if (!SarifConflictsOut.empty() && !parentDirectoryExists(SarifConflictsOut)) { + llvm::errs() << ToolName << ": " + << llvm::formatv(CannotWriteFile, SarifConflictsOut) << "\n"; + return 1; + } + + // Read all inputs. + std::vector<clang::tooling::TranslationUnitReplacements> TUs; + TUs.reserve(InputFiles.size()); + for (const std::string &Path : InputFiles) { + clang::tooling::TranslationUnitReplacements TU; + if (!readInput(Path, TU)) + return 1; + TUs.push_back(std::move(TU)); + } + + // Pre-deduplicate identical replacements across all input TUs. + // + // This loop keeps a running set of every (file, offset, length, text) + // tuple already kept across all TUs and drops + // any later Replacement that matches one already kept, so each distinct + // Replacement is considered exactly once below. The first occurrence (in + // input-file order, then within-file order) wins; later duplicates are + // byte-identical to it, so which one is "first" is observationally moot. + { + std::set<clang::tooling::Replacement> SeenKeys; + for (auto &TU : TUs) { + std::vector<clang::tooling::Replacement> Unique; + Unique.reserve(TU.Replacements.size()); + for (const clang::tooling::Replacement &R : TU.Replacements) { + if (SeenKeys.insert(R).second) + Unique.push_back(R); + } + TU.Replacements = std::move(Unique); + } + } + + // Determine which input files exist on disk. A Replacement targeting a + // file that doesn't exist can never be applied, so every Replacement + // targeting that file is excluded from the merged output. + std::set<std::string> MissingFiles; + { + std::set<std::string> AllFiles; + for (const auto &TU : TUs) + for (const auto &R : TU.Replacements) + AllFiles.insert(R.getFilePath().str()); + for (const std::string &F : AllFiles) + if (!llvm::sys::fs::exists(F)) + MissingFiles.insert(F); + } + for (const std::string &F : MissingFiles) + llvm::errs() << ToolName << ": " << llvm::formatv(MissingReplacementFile, F) + << "\n"; + + // Split every surviving-candidate Replacement by file. Zero-length + // insertions go straight into SurvivorsByFile — they can never overlap + // anything, so they're never at risk of being dropped. Length > 0 entries + // go into InputKeysByFile, the input to buildConflictClusters, which is + // the sole authority on which of them conflict. + std::map<std::string, std::set<clang::tooling::Replacement>> SurvivorsByFile; + std::map<std::string, std::set<clang::tooling::Replacement>> InputKeysByFile; + for (const auto &TU : TUs) { + for (const auto &R : TU.Replacements) { + if (MissingFiles.count(R.getFilePath().str())) + continue; + if (R.getLength() == 0) + SurvivorsByFile[R.getFilePath().str()].insert(R); + else + InputKeysByFile[R.getFilePath().str()].insert(R); + } + } + + // Build conflict clusters — the sole authority on both what gets dropped + // and what gets reported. There is no separate merge step to disagree + // with it. + std::vector<std::vector<clang::tooling::Replacement>> Clusters = + buildConflictClusters(InputKeysByFile); + + // Every Replacement that's a member of a (size > 1) cluster is dropped; + // everything else in InputKeysByFile survives into SurvivorsByFile. + std::set<clang::tooling::Replacement> ClusterMembers; + for (const auto &Cluster : Clusters) + for (const clang::tooling::Replacement &K : Cluster) + ClusterMembers.insert(K); + for (auto &Entry : InputKeysByFile) + for (const clang::tooling::Replacement &R : Entry.second) + if (!ClusterMembers.count(R)) + SurvivorsByFile[Entry.first].insert(R); + + // Flatten SurvivorsByFile into the merged output. Iterating a std::map of + // std::sets yields (file, then offset/length/text) order deterministically, + // regardless of argv or input-file order. + MergedReplacements OutDoc; + OutDoc.MainSourceFile = computeMainSourceFile(TUs); + for (auto &Entry : SurvivorsByFile) + for (const clang::tooling::Replacement &R : Entry.second) + OutDoc.Replacements.push_back(R); + + // Emit stderr cluster lines. Clusters was sorted by (file, min-offset) + // ascending inside buildConflictClusters. + emitConflictClusterLines(Clusters); + + // When --sarif-conflicts-out=<path> was supplied, write the SARIF + // document. An empty Clusters still produces a well-formed SARIF with + // results: [] — the file's presence is the signal that conflict + // reporting was requested. Flag-omitted skips emission entirely; no file + // is created at any path. + if (!SarifConflictsOut.empty()) { + if (llvm::Error E = emitConflictSarif(SarifConflictsOut, Clusters)) { + llvm::errs() << ToolName << ": " << llvm::toString(std::move(E)) << "\n"; + return 1; + } + } + + // Write merged YAML (truncate-and-overwrite per spec). + std::error_code EC; + llvm::raw_fd_ostream OutStream(OutputFile, EC, llvm::sys::fs::OF_Text); + if (EC) { + llvm::errs() << ToolName << ": " + << llvm::formatv(CannotWriteOutput, OutputFile, EC.message()) + << "\n"; + return 1; + } + llvm::yaml::Output YAML(OutStream); + YAML << OutDoc; + OutStream.flush(); + if (OutStream.has_error()) { + llvm::errs() << ToolName << ": " + << llvm::formatv(WriteErrorOnFile, OutputFile) << "\n"; + return 1; + } + + return 0; +} diff --git a/clang-tools-extra/test/CMakeLists.txt b/clang-tools-extra/test/CMakeLists.txt index 22f227a891f82..49794ff5111a3 100644 --- a/clang-tools-extra/test/CMakeLists.txt +++ b/clang-tools-extra/test/CMakeLists.txt @@ -33,6 +33,7 @@ set(CLANG_TOOLS_TEST_DEPS clang-move clang-query clang-reorder-fields + clang-ssaf-src-edit-merge find-all-symbols modularize pp-trace diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-q-offset-30.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-q-offset-30.tpl.yaml new file mode 100644 index 0000000000000..0e263528352da --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-q-offset-30.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 30 + Length: 3 + ReplacementText: 'Q' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-y.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-y.tpl.yaml new file mode 100644 index 0000000000000..315069302d5af --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-y.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'Y' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-z.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-z.tpl.yaml new file mode 100644 index 0000000000000..b907f0a3dad9a --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/conflict-text-z.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'Z' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/empty.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/empty.tpl.yaml new file mode 100644 index 0000000000000..cb2997ae678b4 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/empty.tpl.yaml @@ -0,0 +1,4 @@ +--- +MainSourceFile: '@FOO@' +Replacements: [] +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/internal-conflict.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/internal-conflict.tpl.yaml new file mode 100644 index 0000000000000..cf7914d3ddca9 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/internal-conflict.tpl.yaml @@ -0,0 +1,12 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'X' + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'Y' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-a.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-a.tpl.yaml new file mode 100644 index 0000000000000..b461ee5b741a1 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-a.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'X' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-b.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-b.tpl.yaml new file mode 100644 index 0000000000000..6dc742bfeb36f --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/non-overlapping-b.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 20 + Length: 5 + ReplacementText: 'Y' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-cluster-of-three.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-cluster-of-three.tpl.yaml new file mode 100644 index 0000000000000..f506d7de270c9 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-cluster-of-three.tpl.yaml @@ -0,0 +1,16 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 5 + ReplacementText: 'X' + - FilePath: '@FOO@' + Offset: 12 + Length: 8 + ReplacementText: 'Y' + - FilePath: '@FOO@' + Offset: 18 + Length: 3 + ReplacementText: 'Z' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-distinct-offsets.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-distinct-offsets.tpl.yaml new file mode 100644 index 0000000000000..808be0108fd70 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/range-overlap-distinct-offsets.tpl.yaml @@ -0,0 +1,12 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 5 + ReplacementText: 'X' + - FilePath: '@FOO@' + Offset: 12 + Length: 3 + ReplacementText: 'Y' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-non-zero-length-replacement.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-non-zero-length-replacement.tpl.yaml new file mode 100644 index 0000000000000..a8a7cbfefc3e8 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-non-zero-length-replacement.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 7 + ReplacementText: 'foo' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement-offset-30.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement-offset-30.tpl.yaml new file mode 100644 index 0000000000000..ba031fead786a --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement-offset-30.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 30 + Length: 3 + ReplacementText: 'P' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement.tpl.yaml new file mode 100644 index 0000000000000..b461ee5b741a1 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-replacement.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 3 + ReplacementText: 'X' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-zero-length-insert.tpl.yaml b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-zero-length-insert.tpl.yaml new file mode 100644 index 0000000000000..115a18ad2b194 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/Inputs/single-zero-length-insert.tpl.yaml @@ -0,0 +1,8 @@ +--- +MainSourceFile: '@FOO@' +Replacements: + - FilePath: '@FOO@' + Offset: 10 + Length: 0 + ReplacementText: '.data()' +... diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/bad-args.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/bad-args.test new file mode 100644 index 0000000000000..f182f107ec289 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/bad-args.test @@ -0,0 +1,10 @@ +# Required-arg checks for clang-ssaf-src-edit-merge. + +# Missing -o. +# RUN: not clang-ssaf-src-edit-merge dummy.yaml 2>&1 | FileCheck %s --check-prefix=NEED-O +# NEED-O: -o +# NEED-O-SAME: must be specified + +# Missing positional inputs. +# RUN: not clang-ssaf-src-edit-merge -o /tmp/out.yaml 2>&1 | FileCheck %s --check-prefix=NEED-INPUTS +# NEED-INPUTS: at least 1 positional argument diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-preserves-unrelated-zero-length-insert.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-preserves-unrelated-zero-length-insert.test new file mode 100644 index 0000000000000..871498e4d3e33 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-preserves-unrelated-zero-length-insert.test @@ -0,0 +1,44 @@ +# A zero-length insertion at the same offset as an +# unrelated conflict cluster must survive drop-all filtering. +# +# Three inputs target foo.cpp offset 10: a zero-length insertion +# ('.data()', out of scope for drop-all per spec) plus two conflicting +# length-3 replacements ('X' and 'Y'). The insertion never overlaps +# either replacement (zero-length ranges never overlap under +# Range::overlapsWith), so it is not part of the {X, Y} conflict cluster +# and must be preserved in the merged output even though it shares the +# cluster's (file, offset) coordinate. +# +# Drop-all filtering must key removal on the full replacement identity +# (file, offset, length, text), not just (file, offset), or it will +# collaterally delete this unrelated insertion. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-zero-length-insert.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/b.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/c.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# The zero-length '.data()' insertion survives; the conflicting X/Y +# replacements are both dropped. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: Replacements: +# CHECK: Length: 0 +# CHECK-NEXT: ReplacementText: '.data()' +# CHECK-NOT: ReplacementText: +# CHECK: ... + +# Cluster summary line still reports the X/Y conflict at foo.cpp:10. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 2 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-stderr-without-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-stderr-without-sarif.test new file mode 100644 index 0000000000000..1067c49e37a54 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/conflict-stderr-without-sarif.test @@ -0,0 +1,29 @@ +# Same conflicting inputs as two-input-conflict.test, but the user does +# NOT pass --sarif-conflicts-out=. Stderr cluster line is still emitted +# and drop-all still applies; no SARIF file is created at any path. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# No SARIF file at the path a paired test would have used. +# RUN: ! test -e %t.dir/conflicts.sarif + +# Drop-all still applies even without the SARIF flag. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +# Stderr cluster line is still emitted (back-compat regression guard). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 2 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/empty-input.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/empty-input.test new file mode 100644 index 0000000000000..ba08d60ade242 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/empty-input.test @@ -0,0 +1,24 @@ +# When all inputs have empty Replacements sequences, the output is also +# a valid TranslationUnitReplacements with an empty Replacements sequence. +# Per spec the MainSourceFile is the shared value when all inputs agree. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/empty.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/empty.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: MainSourceFile: +# CHECK-SAME: foo.cpp +# CHECK: Replacements: +# CHECK-SAME: [] +# CHECK-NOT: FilePath: + +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/help.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/help.test new file mode 100644 index 0000000000000..9842c1e58f5f5 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/help.test @@ -0,0 +1,9 @@ +# --help smoke test for clang-ssaf-src-edit-merge. + +# RUN: clang-ssaf-src-edit-merge --help 2>&1 | FileCheck %s + +# CHECK: clang-ssaf-src-edit-merge +# CHECK: USAGE +# CHECK-SAME: input.yaml +# CHECK: -o +# CHECK-SAME: Output path for the merged YAML diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/identical-overlap.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/identical-overlap.test new file mode 100644 index 0000000000000..99427f0459b26 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/identical-overlap.test @@ -0,0 +1,26 @@ +# Identical replacements at the same (file, offset, length, text) from two +# inputs collapse to one in the merged output. No "conflict:" line because +# this is a textbook dedup, not a conflict. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# Use the same single-replacement template for both inputs. +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Exactly one Replacement entry survived. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: Replacements: +# CHECK: ReplacementText: X +# CHECK-NOT: ReplacementText: +# CHECK: ... + +# No tool-level conflict line. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/lit.local.cfg b/clang-tools-extra/test/clang-ssaf-src-edit-merge/lit.local.cfg new file mode 100644 index 0000000000000..0ed7eee8f8bf4 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/lit.local.cfg @@ -0,0 +1 @@ +config.suffixes = [".test"] diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/malformed-yaml.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/malformed-yaml.test new file mode 100644 index 0000000000000..0ccc6147219a6 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/malformed-yaml.test @@ -0,0 +1,19 @@ +# Malformed-YAML and empty-file inputs cause a non-zero exit with a clear +# stderr message. Verifies the spec's "an input file [...] does not parse +# as TranslationUnitReplacements" error path. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir + +# Input is not valid YAML at all. +# RUN: echo '{ this is not yaml ::: %@ }' > %t.dir/garbage.yaml +# RUN: not clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/garbage.yaml \ +# RUN: 2> %t.dir/stderr.txt +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=BAD %s +# BAD: clang-ssaf-src-edit-merge: +# BAD-SAME: garbage.yaml + +# An empty file: the YAML traits will parse to a default-initialized +# TranslationUnitReplacements with no error. We accept that; the merged +# output is then empty and the tool exits 0 (covered indirectly elsewhere). +# Behavior on truly malformed YAML is the load-bearing test here. diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/no-conflict-sarif-empty.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/no-conflict-sarif-empty.test new file mode 100644 index 0000000000000..9575025ee00f3 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/no-conflict-sarif-empty.test @@ -0,0 +1,35 @@ +# No-conflict input with --sarif-conflicts-out=: SARIF is still written but +# with results: [] (empty array). The merged YAML retains both replacements. +# Tool exits 0; stderr contains no `conflict:` line. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/non-overlapping-a.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/non-overlapping-b.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml 2> %t.dir/stderr.txt + +# Both inputs' replacements survive unchanged. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-DAG: ReplacementText: X +# MERGED-DAG: ReplacementText: Y + +# SARIF was written with version 2.1.0 and an empty results array. +# llvm::json::Object emits keys alphabetically, so $schema, results, +# tool come first inside runs[0] and version is the last top-level key. +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "$schema": "https://json.schemastore.org/sarif-2.1.0.json" +# SARIF: "results": [] +# SARIF: "name": "clang-ssaf-src-edit-merge" +# SARIF: "version": "2.1.0" + +# Stderr contains no `conflict:` line. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=STDERR \ +# RUN: --allow-empty %s +# STDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/non-overlapping.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/non-overlapping.test new file mode 100644 index 0000000000000..13310acae189e --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/non-overlapping.test @@ -0,0 +1,31 @@ +# Non-overlapping replacements from two inputs both survive the merge. +# Conflicts are detected per-(file, offset) so disjoint offsets yield no +# tool-level "conflict:" line, and exit is 0. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# Real source file required so groupReplacements doesn't drop the entries. +# 80 bytes is plenty for both Offset=10 and Offset=20. +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/non-overlapping-a.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/non-overlapping-b.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Output contains both replacements. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED: MainSourceFile: +# MERGED-SAME: foo.cpp +# MERGED: Offset: +# MERGED-SAME: 10 +# MERGED: ReplacementText: X +# MERGED: Offset: +# MERGED-SAME: 20 +# MERGED: ReplacementText: Y + +# No "conflict:" lines on stderr. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/order-invariance.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/order-invariance.test new file mode 100644 index 0000000000000..9a10eba5c7fc1 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/order-invariance.test @@ -0,0 +1,19 @@ +# The merge SHALL be order-invariant: permuting the input argv yields a +# byte-equivalent output (modulo internal sort stabilization). Verifies +# the deterministic sort in flattenFileChanges and the dedup-by-key +# logic don't inadvertently depend on input order. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/non-overlapping-a.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/non-overlapping-b.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# Run the tool twice with diff erent argv orders. +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged-ab.yaml %t.dir/a.yaml %t.dir/b.yaml +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged-ba.yaml %t.dir/b.yaml %t.dir/a.yaml + +# Outputs are byte-identical. +# RUN: diff %t.dir/merged-ab.yaml %t.dir/merged-ba.yaml diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/parent-dir-missing.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/parent-dir-missing.test new file mode 100644 index 0000000000000..1001363d1302e --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/parent-dir-missing.test @@ -0,0 +1,19 @@ +# Output path's parent directory does not exist → tool exits non-zero +# with a clear "cannot write" diagnostic naming the path. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# Note: %t.dir/no-such-subdir does NOT exist. The output path therefore +# cannot be opened and the tool must fail. +# RUN: not clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/no-such-subdir/merged.yaml %t.dir/a.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# RUN: FileCheck --input-file=%t.dir/stderr.txt %s +# CHECK: clang-ssaf-src-edit-merge: +# CHECK-SAME: cannot write +# CHECK-SAME: no-such-subdir diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-identical-non-zero-length.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-identical-non-zero-length.test new file mode 100644 index 0000000000000..4444ea0703445 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-identical-non-zero-length.test @@ -0,0 +1,28 @@ +# Identical non-zero-length replacements at the same (file, offset, length, text) +# from two inputs collapse to ONE entry. The pre-existing identical-overlap.test +# also covers this through library-side overlap detection; this test pins the +# pre-dedup pass's contract that the dedup applies regardless of length. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# Two inputs with byte-identical Length: 7 replacements at offset 10. +# RUN: cp %S/Inputs/single-non-zero-length-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/single-non-zero-length-replacement.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Exactly ONE Replacement entry survived. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: Replacements: +# CHECK: Length: 7 +# CHECK-NEXT: ReplacementText: foo +# CHECK-NOT: ReplacementText: +# CHECK: ... + +# No tool-level conflict line. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-zero-length-insert.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-zero-length-insert.test new file mode 100644 index 0000000000000..a5f9b93b92715 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/pre-dedup-zero-length-insert.test @@ -0,0 +1,33 @@ +# Identical zero-length insertions at the same (file, offset, text) from two +# inputs (e.g., a `.data()` rewrite in a header included by N TUs) collapse +# to ONE entry in the merged output, not concatenated into '.data().data()'. +# This pins the libJP2-trigger shape from iter-09: the runaway-chain bug +# where clang-apply-replacements' groupReplacements skipped dedup for plain +# TUReplacements and AtomicChange::replace stacked the duplicates. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# Two inputs with byte-identical zero-length insertions at offset 10. +# RUN: cp %S/Inputs/single-zero-length-insert.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/single-zero-length-insert.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Exactly ONE Replacement entry survived — the duplicate from b.yaml is +# collapsed by the pre-dedup pass before mergeAndDeduplicate runs. +# Pre-fix this would have produced two stacked inserts whose AtomicChange +# concatenation yielded '.data().data()'. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: Replacements: +# CHECK: Length: 0 +# CHECK-NEXT: ReplacementText: '.data()' +# CHECK-NOT: ReplacementText: +# CHECK: ... + +# No tool-level conflict line — identical entries are not conflicts. +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-cluster-of-three-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-cluster-of-three-sarif.test new file mode 100644 index 0000000000000..e4b13b874d5e1 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-cluster-of-three-sarif.test @@ -0,0 +1,63 @@ +## Cluster-of-3+ range-overlap regression guard. One input with three +## replacements: (10, 5, "X"), (12, 8, "Y"), (18, 3, "Z"). Byte ranges +## are [10, 15), [12, 20), and [18, 21). +## +## - (10, 5) and (12, 8) overlap DIRECTLY on bytes 12..14. +## - (12, 8) and (18, 3) overlap DIRECTLY on bytes 18..19. +## - (10, 5) and (18, 3) do NOT overlap directly (18 >= 15) but DO +## overlap TRANSITIVELY via (12, 8). The cluster algorithm merges +## them via lastEnd propagation: after (10, 5) lastEnd is 15; +## (12, 8) joins because 12 < 15, lastEnd becomes max(15, 20) = 20; +## (18, 3) joins because 18 < 20. +## +## This is the regression guard for transitive merging — it cannot be +## exercised by any pairwise-only fixture. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/range-overlap-cluster-of-three.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml 2> %t.dir/stderr.txt + +## Drop-all: zero replacements survive at offsets 10, 12, OR 18. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +## One report-side result at the cluster's minimum offset (10). +## relatedLocations sorted by (byteLength, text) ascending so the +## length-3 "Z" entry comes first, then length-5 "X", then length-8 +## "Y". Within each entry json::Object emits keys in alphabetical +## order: id, message, physicalLocation; within region: byteLength, +## byteOffset. +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "results": +# SARIF: "level": "error" +# SARIF: "byteOffset": 10 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "text": "candidate edit: \"Z\"" +# SARIF: "byteLength": 3 +# SARIF-NEXT: "byteOffset": 18 +# SARIF: "id": 2 +# SARIF: "text": "candidate edit: \"X\"" +# SARIF: "byteLength": 5 +# SARIF-NEXT: "byteOffset": 10 +# SARIF: "id": 3 +# SARIF: "text": "candidate edit: \"Y\"" +# SARIF: "byteLength": 8 +# SARIF-NEXT: "byteOffset": 12 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" + +## Stderr: one cluster line at foo.cpp:10 (cluster min offset), +## skipped 3 (full cluster size, not size - 1). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 3 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-conflict-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-conflict-sarif.test new file mode 100644 index 0000000000000..d5fe203df7f44 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/range-overlap-conflict-sarif.test @@ -0,0 +1,53 @@ +# Range-overlap conflict with distinct start offsets — one input +# contains two replacements at (10, 5, "X") and (12, 3, "Y"). Byte +# ranges [10, 15) and [12, 15) overlap on bytes 12..14, forming one +# cluster of two. Drop-all removes both members at their respective +# offsets; SARIF has one result at byteOffset 10 (cluster min) with two +# relatedLocations sorted by (byteLength, text) ascending so the +# length-3 "Y" entry precedes the length-5 "X" entry. +# +# This is the mandatory regression guard for the range-overlap cluster +# definition — it cannot be exercised by any same-offset fixture. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/range-overlap-distinct-offsets.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml 2> %t.dir/stderr.txt + +# Drop-all: zero replacements survive at offsets 10 OR 12. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +# Run the SARIF assertions. One result at byteOffset 10 (cluster min). +# relatedLocations is sorted by (byteLength, text) so length=3 "Y" comes +# before length=5 "X". Within each entry json::Object emits keys in +# alphabetical order: id, message, physicalLocation; within region: +# byteLength, byteOffset. +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "results": +# SARIF: "level": "error" +# SARIF: "byteOffset": 10 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "text": "candidate edit: \"Y\"" +# SARIF: "byteLength": 3 +# SARIF-NEXT: "byteOffset": 12 +# SARIF: "id": 2 +# SARIF: "text": "candidate edit: \"X\"" +# SARIF: "byteLength": 5 +# SARIF-NEXT: "byteOffset": 10 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" + +# Stderr: one cluster line at foo.cpp:10 (cluster min offset). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 2 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/sarif-out-parent-dir-missing.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/sarif-out-parent-dir-missing.test new file mode 100644 index 0000000000000..0efdbd77d5377 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/sarif-out-parent-dir-missing.test @@ -0,0 +1,21 @@ +# --sarif-conflicts-out=<path> where <path>'s parent directory does not +# exist → tool exits non-zero with a one-line stderr diagnostic naming +# the SARIF path. Mirrors parent-dir-missing.test for -o. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# Note: %t.dir/nonexistent-subdir does NOT exist. +# RUN: not clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/nonexistent-subdir/conflicts.sarif \ +# RUN: %t.dir/a.yaml 2> %t.dir/stderr.txt + +# RUN: FileCheck --input-file=%t.dir/stderr.txt %s +# CHECK: clang-ssaf-src-edit-merge: +# CHECK-SAME: cannot write +# CHECK-SAME: nonexistent-subdir +# CHECK-SAME: conflicts.sarif diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input-internal-conflict.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input-internal-conflict.test new file mode 100644 index 0000000000000..bd7bd68e7b632 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input-internal-conflict.test @@ -0,0 +1,26 @@ +# A single input file containing two overlapping replacements (same file, +# offset, and length but diff erent replacement text). The library detects +# this as a within-input conflict and drops one; the tool reports the +# cluster on stderr and exits 0. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/internal-conflict.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Drop-all: zero replacements survive at the conflict offset (both the X +# and Y candidates are removed). +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK-NOT: ReplacementText: + +# Cluster summary line emitted with the full cluster size (2). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 2 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input.test new file mode 100644 index 0000000000000..f571dfeb72a3d --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/single-input.test @@ -0,0 +1,23 @@ +# Single input passes through (modulo deterministic ordering). Spec: +# "If exactly one input is given, the tool SHALL still produce a one-run +# merged document equivalent to that input." + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK: MainSourceFile: +# CHECK-SAME: foo.cpp +# CHECK: Offset: +# CHECK-SAME: 10 +# CHECK: ReplacementText: X + +# RUN: FileCheck --input-file=%t.dir/stderr.txt --allow-empty --check-prefix=NOSTDERR %s +# NOSTDERR-NOT: conflict: diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-all-conflict.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-all-conflict.test new file mode 100644 index 0000000000000..3c1611453bc7b --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-all-conflict.test @@ -0,0 +1,30 @@ +# Three inputs all conflict pairwise at the same (file, offset, length). +# Per spec ("All inputs conflict pairwise"): under the tool's drop-all +# policy every candidate is removed from the merged YAML; the tool emits +# a "conflict:" line for the (file, offset) cluster with the full cluster +# size (3) and exits 0. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: cp %S/Inputs/conflict-text-z.tpl.yaml %t.dir/c.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Drop-all: zero replacements survive at the conflict offset. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK-NOT: ReplacementText: + +# Tool's conflict summary names the cluster with the full cluster size (3). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 3 +# CONFLICT-SAME: foo.cpp:10 diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-conflict-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-conflict-sarif.test new file mode 100644 index 0000000000000..b45299601c80e --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/three-input-conflict-sarif.test @@ -0,0 +1,33 @@ +# Three inputs all conflict pairwise at (foo.cpp, 10) — drop-all removes +# all three from the merged YAML; SARIF result lists three relatedLocations. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: cp %S/Inputs/conflict-text-z.tpl.yaml %t.dir/c.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml 2> %t.dir/stderr.txt + +# Drop-all unchanged: all three candidates are removed at the conflict offset. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +# SARIF result lists three relatedLocations (one per cluster member). +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "results": +# SARIF: "level": "error" +# SARIF: "byteOffset": 10 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "id": 2 +# SARIF: "id": 3 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" +# SARIF: "version": "2.1.0" diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-conflicts- diff erent-offsets-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-conflicts- diff erent-offsets-sarif.test new file mode 100644 index 0000000000000..6f86a54d561ae --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-conflicts- diff erent-offsets-sarif.test @@ -0,0 +1,43 @@ +# Four inputs producing two distinct conflict clusters in the same file: +# cluster {X, Y} at offset 10 and cluster {P, Q} at offset 30. Drop-all +# removes all four; SARIF has two results, ordered by (file, min-offset) +# ascending so the offset-10 cluster precedes the offset-30 cluster. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: cp %S/Inputs/single-replacement-offset-30.tpl.yaml %t.dir/c.yaml +# RUN: cp %S/Inputs/conflict-text-q-offset-30.tpl.yaml %t.dir/d.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml %t.dir/d.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml %t.dir/c.yaml %t.dir/d.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Drop-all: zero replacements survive at either conflict offset. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +# Two SARIF results in spec-pinned (file, min-offset) order — byteOffset 10 +# first, byteOffset 30 second. Each has two relatedLocations entries +# (sorted by (byteLength, text) — id 1 then id 2 within each result). +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "results": +# SARIF: "level": "error" +# SARIF: "byteOffset": 10 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "id": 2 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" +# SARIF: "level": "error" +# SARIF: "byteOffset": 30 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "id": 2 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict-sarif.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict-sarif.test new file mode 100644 index 0000000000000..43c6093ed3ed7 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict-sarif.test @@ -0,0 +1,39 @@ +# Two inputs with overlapping replacements (same offset, diff erent text) — +# drop-all removes both from the merged YAML, the tool emits a one-line +# stderr cluster summary, AND writes a SARIF 2.1.0 document at the path +# given by --sarif-conflicts-out= containing one error-level result with +# two relatedLocations (one per dropped candidate). + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge \ +# RUN: -o %t.dir/merged.yaml \ +# RUN: --sarif-conflicts-out=%t.dir/conflicts.sarif \ +# RUN: %t.dir/a.yaml %t.dir/b.yaml 2> %t.dir/stderr.txt + +# Drop-all unchanged: zero replacements survive at the conflict offset. +# RUN: FileCheck --input-file=%t.dir/merged.yaml --check-prefix=MERGED %s +# MERGED-NOT: ReplacementText: + +# SARIF doc has the expected top-level shape and exactly one result. Note: +# llvm::json::Object emits its keys in alphabetical order, so within each +# result the field order is level, locations, message, relatedLocations, +# ruleId. Top-level keys are $schema, runs, version (alphabetical too). +# RUN: FileCheck --input-file=%t.dir/conflicts.sarif --check-prefix=SARIF %s +# SARIF: "$schema": "https://json.schemastore.org/sarif-2.1.0.json" +# SARIF: "results": +# SARIF: "level": "error" +# SARIF: "uri": "file://{{.*}}/foo.cpp" +# SARIF: "byteOffset": 10 +# SARIF: "relatedLocations": +# SARIF: "id": 1 +# SARIF: "id": 2 +# SARIF: "ruleId": "clang-reforge-replacement-conflict" +# SARIF: "name": "clang-ssaf-src-edit-merge" +# SARIF: "version": "2.1.0" diff --git a/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict.test b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict.test new file mode 100644 index 0000000000000..b6e8fa5a5fae8 --- /dev/null +++ b/clang-tools-extra/test/clang-ssaf-src-edit-merge/two-input-conflict.test @@ -0,0 +1,29 @@ +# Two inputs with overlapping replacements (same file, same offset, same +# length, but diff erent replacement text) — under the tool's drop-all +# conflict policy both candidates are removed from the merged YAML; the +# tool emits one "conflict:" cluster line for that (file, offset) with the +# full cluster size, and exits 0. + +# RUN: rm -rf %t.dir +# RUN: mkdir -p %t.dir +# RUN: printf '%%80s' '' > %t.dir/foo.cpp + +# RUN: cp %S/Inputs/single-replacement.tpl.yaml %t.dir/a.yaml +# RUN: cp %S/Inputs/conflict-text-y.tpl.yaml %t.dir/b.yaml +# RUN: sed -i'' -e "s|@FOO@|%/t.dir/foo.cpp|g" %t.dir/a.yaml %t.dir/b.yaml + +# RUN: clang-ssaf-src-edit-merge -o %t.dir/merged.yaml %t.dir/a.yaml %t.dir/b.yaml \ +# RUN: 2> %t.dir/stderr.txt + +# Drop-all: zero replacements survive at the conflict offset. +# RUN: FileCheck --input-file=%t.dir/merged.yaml %s +# CHECK-NOT: ReplacementText: + +# Tool emits the cluster summary on stderr (alongside the library's own +# detailed message which we do not assert on, since it is owned by +# clang-apply-replacements and may evolve). +# RUN: FileCheck --input-file=%t.dir/stderr.txt --check-prefix=CONFLICT %s +# CONFLICT: conflict: +# CONFLICT-SAME: skipped +# CONFLICT-SAME: 2 +# CONFLICT-SAME: foo.cpp:10 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
