From: Daniel Jasper Date: Wed, 23 Sep 2015 08:30:47 +0000 (+0000) Subject: clang-format: Add initial #include sorting capabilities. X-Git-Url: https://granicus.if.org/sourcecode?a=commitdiff_plain;h=30ddf33877ec91815bee893b05850fc9b87fc09e;p=clang clang-format: Add initial #include sorting capabilities. To implement this nicely, add a function that merges two sets of replacements that are meant to be done in sequence. This functionality will also be useful for other applications, e.g. formatting the result of clang-tidy fixes. git-svn-id: https://llvm.org/svn/llvm-project/cfe/trunk@248367 91177308-0d34-0410-b5e6-96231b3b80d8 --- diff --git a/include/clang/Format/Format.h b/include/clang/Format/Format.h index 50faa8a565..6c4a94b8de 100644 --- a/include/clang/Format/Format.h +++ b/include/clang/Format/Format.h @@ -571,6 +571,12 @@ std::error_code parseConfiguration(StringRef Text, FormatStyle *Style); /// \brief Gets configuration in a YAML string. std::string configurationAsText(const FormatStyle &Style); +/// \brief Returns the replacements necessary to sort all #include blocks that +/// are affected by 'Ranges'. +tooling::Replacements sortIncludes(const FormatStyle &Style, StringRef Code, + ArrayRef Ranges, + StringRef FileName); + /// \brief Reformats the given \p Ranges in the file \p ID. /// /// Each range is extended on either end to its next bigger logic unit, i.e. diff --git a/include/clang/Tooling/Core/Replacement.h b/include/clang/Tooling/Core/Replacement.h index f189e12501..37389ac915 100644 --- a/include/clang/Tooling/Core/Replacement.h +++ b/include/clang/Tooling/Core/Replacement.h @@ -220,6 +220,12 @@ bool applyAllReplacements(const std::vector &Replaces, /// replacements cannot be applied, this returns an empty \c string. std::string applyAllReplacements(StringRef Code, const Replacements &Replaces); +/// \brief Merges two sets of replacements with the second set referring to the +/// code after applying the first set. Within both 'First' and 'Second', +/// replacements must not overlap. +Replacements mergeReplacements(const Replacements &First, + const Replacements &Second); + template Replacement::Replacement(const SourceManager &Sources, const Node &NodeToReplace, StringRef ReplacementText, diff --git a/lib/Format/Format.cpp b/lib/Format/Format.cpp index ae313c57dc..15aa50224b 100644 --- a/lib/Format/Format.cpp +++ b/lib/Format/Format.cpp @@ -310,8 +310,8 @@ template <> struct DocumentListTraits> { return Seq[Index]; } }; -} -} +} // namespace yaml +} // namespace llvm namespace clang { namespace format { @@ -1571,8 +1571,108 @@ private: bool BinPackInconclusiveFunctions; }; +struct IncludeDirective { + StringRef Filename; + StringRef Text; + unsigned Offset; + bool IsAngled; +}; + } // end anonymous namespace +// Determines whether 'Ranges' intersects with ('Start', 'End'). +static bool affectsRange(ArrayRef Ranges, unsigned Start, + unsigned End) { + for (auto Range : Ranges) { + if (Range.getOffset() < End && + Range.getOffset() + Range.getLength() > Start) + return true; + } + return false; +} + +// Sorts a block of includes given by 'Includes' alphabetically adding the +// necessary replacement to 'Replaces'. 'Includes' must be in strict source +// order. +static void sortIncludes(const FormatStyle &Style, + const SmallVectorImpl &Includes, + ArrayRef Ranges, StringRef FileName, + tooling::Replacements &Replaces) { + if (!affectsRange(Ranges, Includes.front().Offset, + Includes.back().Offset + Includes.back().Text.size())) + return; + SmallVector Indices; + for (unsigned i = 0, e = Includes.size(); i != e; ++i) + Indices.push_back(i); + std::sort(Indices.begin(), Indices.end(), [&](unsigned LHSI, unsigned RHSI) { + return Includes[LHSI].Filename < Includes[RHSI].Filename; + }); + + // If the #includes are out of order, we generate a single replacement fixing + // the entire block. Otherwise, no replacement is generated. + bool OutOfOrder = false; + for (unsigned i = 1, e = Indices.size(); i != e; ++i) { + if (Indices[i] != i) { + OutOfOrder = true; + break; + } + } + if (!OutOfOrder) + return; + + std::string result = Includes[Indices[0]].Text; + for (unsigned i = 1, e = Indices.size(); i != e; ++i) { + result += "\n"; + result += Includes[Indices[i]].Text; + } + + // Sorting #includes shouldn't change their total number of characters. + // This would otherwise mess up 'Ranges'. + assert(result.size() == + Includes.back().Offset + Includes.back().Text.size() - + Includes.front().Offset); + + Replaces.insert(tooling::Replacement(FileName, Includes.front().Offset, + result.size(), result)); +} + +tooling::Replacements sortIncludes(const FormatStyle &Style, StringRef Code, + ArrayRef Ranges, + StringRef FileName) { + tooling::Replacements Replaces; + unsigned Prev = 0; + unsigned SearchFrom = 0; + llvm::Regex IncludeRegex(R"(^[\t\ ]*#[\t\ ]*include[^"<]*["<]([^">]*)([">]))"); + SmallVector Matches; + SmallVector IncludesInBlock; + for (;;) { + auto Pos = Code.find('\n', SearchFrom); + StringRef Line = + Code.substr(Prev, (Pos != StringRef::npos ? Pos : Code.size()) - Prev); + if (!Line.endswith("\\")) { + if (IncludeRegex.match(Line, &Matches)) { + bool IsAngled = Matches[2] == ">"; + if (!IncludesInBlock.empty() && + IsAngled != IncludesInBlock.back().IsAngled) { + sortIncludes(Style, IncludesInBlock, Ranges, FileName, Replaces); + IncludesInBlock.clear(); + } + IncludesInBlock.push_back({Matches[1], Line, Prev, Matches[2] == ">"}); + } else if (!IncludesInBlock.empty()) { + sortIncludes(Style, IncludesInBlock, Ranges, FileName, Replaces); + IncludesInBlock.clear(); + } + Prev = Pos + 1; + } + if (Pos == StringRef::npos || Pos + 1 == Code.size()) + break; + SearchFrom = Pos + 1; + } + if (!IncludesInBlock.empty()) + sortIncludes(Style, IncludesInBlock, Ranges, FileName, Replaces); + return Replaces; +} + tooling::Replacements reformat(const FormatStyle &Style, SourceManager &SourceMgr, FileID ID, ArrayRef Ranges, diff --git a/lib/Format/FormatToken.cpp b/lib/Format/FormatToken.cpp index 358a9a48ec..b293cf25c3 100644 --- a/lib/Format/FormatToken.cpp +++ b/lib/Format/FormatToken.cpp @@ -13,8 +13,8 @@ /// //===----------------------------------------------------------------------===// -#include "FormatToken.h" #include "ContinuationIndenter.h" +#include "FormatToken.h" #include "clang/Format/Format.h" #include "llvm/ADT/SmallVector.h" #include "llvm/Support/Debug.h" diff --git a/lib/Tooling/Core/Replacement.cpp b/lib/Tooling/Core/Replacement.cpp index 6d37a49db3..8de362fae8 100644 --- a/lib/Tooling/Core/Replacement.cpp +++ b/lib/Tooling/Core/Replacement.cpp @@ -292,6 +292,138 @@ std::string applyAllReplacements(StringRef Code, const Replacements &Replaces) { return Result; } +namespace { +// Represents a merged replacement, i.e. a replacement consisting of multiple +// overlapping replacements from 'First' and 'Second' in mergeReplacements. +// +// Position projection: +// Offsets and lengths of the replacements can generally refer to two different +// coordinate spaces. Replacements from 'First' refer to the original text +// whereas replacements from 'Second' refer to the text after applying 'First'. +// +// MergedReplacement always operates in the coordinate space of the original +// text, i.e. transforms elements from 'Second' to take into account what was +// changed based on the elements from 'First'. +// +// We can correctly calculate this projection as we look at the replacements in +// order of strictly increasing offsets. +// +// Invariants: +// * We always merge elements from 'First' into elements from 'Second' and vice +// versa. Within each set, the replacements are non-overlapping. +// * We only extend to the right, i.e. merge elements with strictly increasing +// offsets. +class MergedReplacement { +public: + MergedReplacement(const Replacement &R, bool MergeSecond, int D) + : MergeSecond(MergeSecond), Delta(D), FilePath(R.getFilePath()), + Offset(R.getOffset() + (MergeSecond ? 0 : Delta)), Length(R.getLength()), + Text(R.getReplacementText()) { + Delta += MergeSecond ? 0 : Text.size() - Length; + DeltaFirst = MergeSecond ? Text.size() - Length : 0; + } + + // Merges the next element 'R' into this merged element. As we always merge + // from 'First' into 'Second' or vice versa, the MergedReplacement knows what + // set the next element is coming from. + void merge(const Replacement &R) { + if (MergeSecond) { + unsigned REnd = R.getOffset() + Delta + R.getLength(); + unsigned End = Offset + Text.size(); + if (REnd > End) { + Length += REnd - End; + MergeSecond = false; + } + StringRef TextRef = Text; + StringRef Head = TextRef.substr(0, R.getOffset() + Delta - Offset); + StringRef Tail = TextRef.substr(REnd - Offset); + Text = (Head + R.getReplacementText() + Tail).str(); + Delta += R.getReplacementText().size() - R.getLength(); + } else { + unsigned End = Offset + Length; + StringRef RText = R.getReplacementText(); + StringRef Tail = RText.substr(End - R.getOffset()); + Text = (Text + Tail).str(); + if (R.getOffset() + RText.size() > End) { + Length = R.getOffset() + R.getLength() - Offset; + MergeSecond = true; + } else { + Length += R.getLength() - RText.size(); + } + DeltaFirst += RText.size() - R.getLength(); + } + } + + // Returns 'true' if 'R' starts strictly after the MergedReplacement and thus + // doesn't need to be merged. + bool endsBefore(const Replacement &R) const { + if (MergeSecond) + return Offset + Text.size() < R.getOffset() + Delta; + return Offset + Length < R.getOffset(); + } + + // Returns 'true' if an element from the second set should be merged next. + bool mergeSecond() const { return MergeSecond; } + int deltaFirst() const { return DeltaFirst; } + Replacement asReplacement() const { return {FilePath, Offset, Length, Text}; } + +private: + bool MergeSecond; + + // Amount of characters that elements from 'Second' need to be shifted by in + // order to refer to the original text. + int Delta; + + // Sum of all deltas (text-length - length) of elements from 'First' merged + // into this element. This is used to update 'Delta' once the + // MergedReplacement is completed. + int DeltaFirst; + + // Data of the actually merged replacement. FilePath and Offset aren't changed + // as the element is only extended to the right. + const StringRef FilePath; + const unsigned Offset; + unsigned Length; + std::string Text; +}; +} // namespace + +Replacements mergeReplacements(const Replacements &First, + const Replacements &Second) { + if (First.empty() || Second.empty()) + return First.empty() ? Second : First; + + // Delta is the amount of characters that replacements from 'Second' need to + // be shifted so that their offsets refer to the original text. + int Delta = 0; + Replacements Result; + + // Iterate over both sets and always add the next element (smallest total + // Offset) from either 'First' or 'Second'. Merge that element with + // subsequent replacements as long as they overlap. See more details in the + // comment on MergedReplacement. + for (auto FirstI = First.begin(), SecondI = Second.begin(); + FirstI != First.end() || SecondI != Second.end();) { + bool NextIsFirst = SecondI == Second.end() || + FirstI->getOffset() < SecondI->getOffset() + Delta; + MergedReplacement Merged(NextIsFirst ? *FirstI : *SecondI, NextIsFirst, + Delta); + ++(NextIsFirst ? FirstI : SecondI); + + while ((Merged.mergeSecond() && SecondI != Second.end()) || + (!Merged.mergeSecond() && FirstI != First.end())) { + auto &I = Merged.mergeSecond() ? SecondI : FirstI; + if (Merged.endsBefore(*I)) + break; + Merged.merge(*I); + ++I; + } + Delta -= Merged.deltaFirst(); + Result.insert(Merged.asReplacement()); + } + return Result; +} + } // end namespace tooling } // end namespace clang diff --git a/tools/clang-format/CMakeLists.txt b/tools/clang-format/CMakeLists.txt index 6ef0c2280f..bad9ffff02 100644 --- a/tools/clang-format/CMakeLists.txt +++ b/tools/clang-format/CMakeLists.txt @@ -7,7 +7,6 @@ add_clang_executable(clang-format set(CLANG_FORMAT_LIB_DEPS clangBasic clangFormat - clangRewrite clangToolingCore ) diff --git a/tools/clang-format/ClangFormat.cpp b/tools/clang-format/ClangFormat.cpp index 5037e901f3..ce9324e304 100644 --- a/tools/clang-format/ClangFormat.cpp +++ b/tools/clang-format/ClangFormat.cpp @@ -19,7 +19,6 @@ #include "clang/Basic/SourceManager.h" #include "clang/Basic/Version.h" #include "clang/Format/Format.h" -#include "clang/Rewrite/Core/Rewriter.h" #include "llvm/ADT/StringMap.h" #include "llvm/Support/CommandLine.h" #include "llvm/Support/Debug.h" @@ -27,6 +26,7 @@ #include "llvm/Support/Signals.h" using namespace llvm; +using clang::tooling::Replacements; static cl::opt Help("h", cl::desc("Alias for -help"), cl::Hidden); @@ -97,6 +97,10 @@ static cl::opt "clang-format from an editor integration"), cl::init(0), cl::cat(ClangFormatCategory)); +static cl::opt SortIncludes("sort-includes", + cl::desc("Sort touched include lines"), + cl::cat(ClangFormatCategory)); + static cl::list FileNames(cl::Positional, cl::desc("[ ...]"), cl::cat(ClangFormatCategory)); @@ -121,9 +125,14 @@ static bool parseLineRange(StringRef Input, unsigned &FromLine, LineRange.second.getAsInteger(0, ToLine); } -static bool fillRanges(SourceManager &Sources, FileID ID, - const MemoryBuffer *Code, - std::vector &Ranges) { +static bool fillRanges(MemoryBuffer *Code, + std::vector &Ranges) { + FileManager Files((FileSystemOptions())); + DiagnosticsEngine Diagnostics( + IntrusiveRefCntPtr(new DiagnosticIDs), + new DiagnosticOptions); + SourceManager Sources(Diagnostics, Files); + FileID ID = createInMemoryFile("-", Code, Sources, Files); if (!LineRanges.empty()) { if (!Offsets.empty() || !Lengths.empty()) { llvm::errs() << "error: cannot use -lines with -offset/-length\n"; @@ -144,7 +153,9 @@ static bool fillRanges(SourceManager &Sources, FileID ID, SourceLocation End = Sources.translateLineCol(ID, ToLine, UINT_MAX); if (Start.isInvalid() || End.isInvalid()) return true; - Ranges.push_back(CharSourceRange::getCharRange(Start, End)); + unsigned Offset = Sources.getFileOffset(Start); + unsigned Length = Sources.getFileOffset(End) - Offset; + Ranges.push_back(tooling::Range(Offset, Length)); } return false; } @@ -177,7 +188,9 @@ static bool fillRanges(SourceManager &Sources, FileID ID, } else { End = Sources.getLocForEndOfFile(ID); } - Ranges.push_back(CharSourceRange::getCharRange(Start, End)); + unsigned Offset = Sources.getFileOffset(Start); + unsigned Length = Sources.getFileOffset(End) - Offset; + Ranges.push_back(tooling::Range(Offset, Length)); } return false; } @@ -202,13 +215,18 @@ static void outputReplacementXML(StringRef Text) { llvm::outs() << Text.substr(From); } +static void outputReplacementsXML(const Replacements &Replaces) { + for (const auto &R : Replaces) { + outs() << ""; + outputReplacementXML(R.getReplacementText()); + outs() << "\n"; + } +} + // Returns true on error. static bool format(StringRef FileName) { - FileManager Files((FileSystemOptions())); - DiagnosticsEngine Diagnostics( - IntrusiveRefCntPtr(new DiagnosticIDs), - new DiagnosticOptions); - SourceManager Sources(Diagnostics, Files); ErrorOr> CodeOrErr = MemoryBuffer::getFileOrSTDIN(FileName); if (std::error_code EC = CodeOrErr.getError()) { @@ -218,16 +236,27 @@ static bool format(StringRef FileName) { std::unique_ptr Code = std::move(CodeOrErr.get()); if (Code->getBufferSize() == 0) return false; // Empty files are formatted correctly. - FileID ID = createInMemoryFile(FileName, Code.get(), Sources, Files); - std::vector Ranges; - if (fillRanges(Sources, ID, Code.get(), Ranges)) + std::vector Ranges; + if (fillRanges(Code.get(), Ranges)) return true; - FormatStyle FormatStyle = getStyle( Style, (FileName == "-") ? AssumeFilename : FileName, FallbackStyle); + Replacements Replaces; + std::string ChangedCode; + if (SortIncludes) { + Replaces = + sortIncludes(FormatStyle, Code->getBuffer(), Ranges, FileName); + ChangedCode = tooling::applyAllReplacements(Code->getBuffer(), Replaces); + for (const auto &R : Replaces) + Ranges.push_back({R.getOffset(), R.getLength()}); + } else { + ChangedCode = Code->getBuffer().str(); + } + bool IncompleteFormat = false; - tooling::Replacements Replaces = - reformat(FormatStyle, Sources, ID, Ranges, &IncompleteFormat); + Replaces = tooling::mergeReplacements( + Replaces, + reformat(FormatStyle, ChangedCode, Ranges, FileName, &IncompleteFormat)); if (OutputXML) { llvm::outs() << "\ngetOffset() << "' " - << "length='" << I->getLength() << "'>"; - outputReplacementXML(I->getReplacementText()); - llvm::outs() << "\n"; - } + outputReplacementsXML(Replaces); llvm::outs() << "\n"; } else { - Rewriter Rewrite(Sources, LangOptions()); - tooling::applyAllReplacements(Replaces, Rewrite); + std::string FormattedCode = + applyAllReplacements(Code->getBuffer(), Replaces); if (Inplace) { if (FileName == "-") llvm::errs() << "error: cannot use -i when reading from stdin.\n"; - else if (Rewrite.overwriteChangedFiles()) - return true; + else { + std::error_code EC; + raw_fd_ostream FileOut(FileName, EC, llvm::sys::fs::F_Text); + if (EC) { + llvm::errs() << EC.message() << "\n"; + return true; + } + FileOut << FormattedCode; + } } else { if (Cursor.getNumOccurrences() != 0) outs() << "{ \"Cursor\": " << tooling::shiftedCodePosition(Replaces, Cursor) << ", \"IncompleteFormat\": " << (IncompleteFormat ? "true" : "false") << " }\n"; - Rewrite.getEditBuffer(ID).write(outs()); + outs() << FormattedCode; } } return false; diff --git a/tools/clang-format/clang-format.py b/tools/clang-format/clang-format.py index 5cb41fcfa3..1725e8659a 100644 --- a/tools/clang-format/clang-format.py +++ b/tools/clang-format/clang-format.py @@ -72,7 +72,7 @@ def main(): startupinfo.wShowWindow = subprocess.SW_HIDE # Call formatter. - command = [binary, '-style', style, '-cursor', str(cursor)] + command = [binary, '-style', style, '-cursor', str(cursor), '-sort-includes'] if lines != 'all': command.extend(['-lines', lines]) if fallback_style: diff --git a/unittests/Format/CMakeLists.txt b/unittests/Format/CMakeLists.txt index 6d48cf8713..01af435fff 100644 --- a/unittests/Format/CMakeLists.txt +++ b/unittests/Format/CMakeLists.txt @@ -8,6 +8,7 @@ add_clang_unittest(FormatTests FormatTestJS.cpp FormatTestProto.cpp FormatTestSelective.cpp + SortIncludesTest.cpp ) target_link_libraries(FormatTests diff --git a/unittests/Format/SortIncludesTest.cpp b/unittests/Format/SortIncludesTest.cpp new file mode 100644 index 0000000000..cbbc2baad9 --- /dev/null +++ b/unittests/Format/SortIncludesTest.cpp @@ -0,0 +1,108 @@ +//===- unittest/Format/SortIncludesTest.cpp - Include sort unit tests -----===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include "FormatTestUtils.h" +#include "clang/Format/Format.h" +#include "llvm/Support/Debug.h" +#include "gtest/gtest.h" + +#define DEBUG_TYPE "format-test" + +namespace clang { +namespace format { +namespace { + +class SortIncludesTest : public ::testing::Test { +protected: + std::string sort(llvm::StringRef Code) { + std::vector Ranges(1, tooling::Range(0, Code.size())); + std::string Sorted = applyAllReplacements( + Code, sortIncludes(getLLVMStyle(), Code, Ranges, "input.cpp")); + return applyAllReplacements( + Sorted, reformat(getLLVMStyle(), Sorted, Ranges, "input.cpp")); + } +}; + +TEST_F(SortIncludesTest, BasicSorting) { + EXPECT_EQ("#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include \"c.h\"\n", + sort("#include \"a.h\"\n" + "#include \"c.h\"\n" + "#include \"b.h\"\n")); +} + +TEST_F(SortIncludesTest, FixTrailingComments) { + EXPECT_EQ("#include \"a.h\" // comment\n" + "#include \"bb.h\" // comment\n" + "#include \"ccc.h\"\n", + sort("#include \"a.h\" // comment\n" + "#include \"ccc.h\"\n" + "#include \"bb.h\" // comment\n")); +} + +TEST_F(SortIncludesTest, LeadingWhitespace) { + EXPECT_EQ("#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include \"c.h\"\n", + sort(" #include \"a.h\"\n" + " #include \"c.h\"\n" + " #include \"b.h\"\n")); + EXPECT_EQ("#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include \"c.h\"\n", + sort("# include \"a.h\"\n" + "# include \"c.h\"\n" + "# include \"b.h\"\n")); +} + +TEST_F(SortIncludesTest, GreaterInComment) { + EXPECT_EQ("#include \"a.h\"\n" + "#include \"b.h\" // >\n" + "#include \"c.h\"\n", + sort("#include \"a.h\"\n" + "#include \"c.h\"\n" + "#include \"b.h\" // >\n")); +} + +TEST_F(SortIncludesTest, SortsLocallyInEachBlock) { + EXPECT_EQ("#include \"a.h\"\n" + "#include \"c.h\"\n" + "\n" + "#include \"b.h\"\n", + sort("#include \"c.h\"\n" + "#include \"a.h\"\n" + "\n" + "#include \"b.h\"\n")); +} + +TEST_F(SortIncludesTest, HandlesAngledIncludesAsSeparateBlocks) { + EXPECT_EQ("#include \n" + "#include \n" + "#include \"a.h\"\n" + "#include \"c.h\"\n", + sort("#include \n" + "#include \n" + "#include \"c.h\"\n" + "#include \"a.h\"\n")); +} + +TEST_F(SortIncludesTest, HandlesMultilineIncludes) { + EXPECT_EQ("#include \"a.h\"\n" + "#include \"b.h\"\n" + "#include \"c.h\"\n", + sort("#include \"a.h\"\n" + "#include \\\n" + "\"c.h\"\n" + "#include \"b.h\"\n")); +} + +} // end namespace +} // end namespace format +} // end namespace clang diff --git a/unittests/Tooling/RefactoringTest.cpp b/unittests/Tooling/RefactoringTest.cpp index 6c2c16b484..d9a87a5690 100644 --- a/unittests/Tooling/RefactoringTest.cpp +++ b/unittests/Tooling/RefactoringTest.cpp @@ -489,5 +489,98 @@ TEST(DeduplicateTest, detectsConflicts) { } } +class MergeReplacementsTest : public ::testing::Test { +protected: + void mergeAndTestRewrite(StringRef Code, StringRef Intermediate, + StringRef Result, const Replacements &First, + const Replacements &Second) { + // These are mainly to verify the test itself and make it easier to read. + std::string AfterFirst = applyAllReplacements(Code, First); + std::string InSequenceRewrite = applyAllReplacements(AfterFirst, Second); + EXPECT_EQ(Intermediate, AfterFirst); + EXPECT_EQ(Result, InSequenceRewrite); + + tooling::Replacements Merged = mergeReplacements(First, Second); + std::string MergedRewrite = applyAllReplacements(Code, Merged); + EXPECT_EQ(InSequenceRewrite, MergedRewrite); + if (InSequenceRewrite != MergedRewrite) + for (tooling::Replacement M : Merged) + llvm::errs() << M.getOffset() << " " << M.getLength() << " " + << M.getReplacementText() << "\n"; + } + void mergeAndTestRewrite(StringRef Code, const Replacements &First, + const Replacements &Second) { + std::string InSequenceRewrite = + applyAllReplacements(applyAllReplacements(Code, First), Second); + tooling::Replacements Merged = mergeReplacements(First, Second); + std::string MergedRewrite = applyAllReplacements(Code, Merged); + EXPECT_EQ(InSequenceRewrite, MergedRewrite); + if (InSequenceRewrite != MergedRewrite) + for (tooling::Replacement M : Merged) + llvm::errs() << M.getOffset() << " " << M.getLength() << " " + << M.getReplacementText() << "\n"; + } +}; + +TEST_F(MergeReplacementsTest, Offsets) { + mergeAndTestRewrite("aaa", "aabab", "cacabab", + {{"", 2, 0, "b"}, {"", 3, 0, "b"}}, + {{"", 0, 0, "c"}, {"", 1, 0, "c"}}); + mergeAndTestRewrite("aaa", "babaa", "babacac", + {{"", 0, 0, "b"}, {"", 1, 0, "b"}}, + {{"", 4, 0, "c"}, {"", 5, 0, "c"}}); + mergeAndTestRewrite("aaaa", "aaa", "aac", {{"", 1, 1, ""}}, + {{"", 2, 1, "c"}}); + + mergeAndTestRewrite("aa", "bbabba", "bbabcba", + {{"", 0, 0, "bb"}, {"", 1, 0, "bb"}}, {{"", 4, 0, "c"}}); +} + +TEST_F(MergeReplacementsTest, Concatenations) { + // Basic concatenations. It is important to merge these into a single + // replacement to ensure the correct order. + EXPECT_EQ((Replacements{{"", 0, 0, "ab"}}), + mergeReplacements({{"", 0, 0, "a"}}, {{"", 1, 0, "b"}})); + EXPECT_EQ((Replacements{{"", 0, 0, "ba"}}), + mergeReplacements({{"", 0, 0, "a"}}, {{"", 0, 0, "b"}})); + mergeAndTestRewrite("", "a", "ab", {{"", 0, 0, "a"}}, {{"", 1, 0, "b"}}); + mergeAndTestRewrite("", "a", "ba", {{"", 0, 0, "a"}}, {{"", 0, 0, "b"}}); +} + +TEST_F(MergeReplacementsTest, NotChangingLengths) { + mergeAndTestRewrite("aaaa", "abba", "acca", {{"", 1, 2, "bb"}}, + {{"", 1, 2, "cc"}}); + mergeAndTestRewrite("aaaa", "abba", "abcc", {{"", 1, 2, "bb"}}, + {{"", 2, 2, "cc"}}); + mergeAndTestRewrite("aaaa", "abba", "ccba", {{"", 1, 2, "bb"}}, + {{"", 0, 2, "cc"}}); + mergeAndTestRewrite("aaaaaa", "abbdda", "abccda", + {{"", 1, 2, "bb"}, {"", 3, 2, "dd"}}, {{"", 2, 2, "cc"}}); +} + +TEST_F(MergeReplacementsTest, OverlappingRanges) { + mergeAndTestRewrite("aaa", "bbd", "bcbcd", + {{"", 0, 1, "bb"}, {"", 1, 2, "d"}}, + {{"", 1, 0, "c"}, {"", 2, 0, "c"}}); + + mergeAndTestRewrite("aaaa", "aabbaa", "acccca", {{"", 2, 0, "bb"}}, + {{"", 1, 4, "cccc"}}); + mergeAndTestRewrite("aaaa", "aababa", "acccca", + {{"", 2, 0, "b"}, {"", 3, 0, "b"}}, {{"", 1, 4, "cccc"}}); + mergeAndTestRewrite("aaaaaa", "abbbba", "abba", {{"", 1, 4, "bbbb"}}, + {{"", 2, 2, ""}}); + mergeAndTestRewrite("aaaa", "aa", "cc", {{"", 1, 1, ""}, {"", 2, 1, ""}}, + {{"", 0, 2, "cc"}}); + mergeAndTestRewrite("aa", "abbba", "abcbcba", {{"", 1, 0, "bbb"}}, + {{"", 2, 0, "c"}, {"", 3, 0, "c"}}); + + mergeAndTestRewrite("aaa", "abbab", "ccdd", + {{"", 0, 1, ""}, {"", 2, 0, "bb"}, {"", 3, 0, "b"}}, + {{"", 0, 2, "cc"}, {"", 2, 3, "dd"}}); + mergeAndTestRewrite("aa", "babbab", "ccdd", + {{"", 0, 0, "b"}, {"", 1, 0, "bb"}, {"", 2, 0, "b"}}, + {{"", 0, 3, "cc"}, {"", 3, 3, "dd"}}); +} + } // end namespace tooling } // end namespace clang