Make clang-format remove duplicate headers when sorting #includes.

Summary: When sorting #includes, #include directives that have the same text will be deduplicated when sorting #includes, and only the first #include in the duplicate #includes remains. If the `Cursor` is provided and put on a deleted #include, it will be put on the remaining #include in the duplicate #includes.

Reviewers: djasper

Subscribers: cfe-commits, klimek

Differential Revision: https://reviews.llvm.org/D23274

llvm-svn: 278206
This commit is contained in:
Eric Liu 2016-08-10 09:32:23 +00:00
parent e4195dc803
commit a992afe809
4 changed files with 162 additions and 35 deletions

View File

@ -1223,15 +1223,50 @@ static bool affectsRange(ArrayRef<tooling::Range> Ranges, unsigned Start,
return false;
}
// Sorts a block of includes given by 'Includes' alphabetically adding the
// necessary replacement to 'Replaces'. 'Includes' must be in strict source
// order.
// Returns a pair (Index, OffsetToEOL) describing the position of the cursor
// before sorting/deduplicating. Index is the index of the include under the
// cursor in the original set of includes. If this include has duplicates, it is
// the index of the first of the duplicates as the others are going to be
// removed. OffsetToEOL describes the cursor's position relative to the end of
// its current line.
// If `Cursor` is not on any #include, `Index` will be UINT_MAX.
static std::pair<unsigned, unsigned>
FindCursorIndex(const SmallVectorImpl<IncludeDirective> &Includes,
const SmallVectorImpl<unsigned> &Indices, unsigned Cursor) {
unsigned CursorIndex = UINT_MAX;
unsigned OffsetToEOL = 0;
for (int i = 0, e = Includes.size(); i != e; ++i) {
unsigned Start = Includes[Indices[i]].Offset;
unsigned End = Start + Includes[Indices[i]].Text.size();
if (!(Cursor >= Start && Cursor < End))
continue;
CursorIndex = Indices[i];
OffsetToEOL = End - Cursor;
// Put the cursor on the only remaining #include among the duplicate
// #includes.
while (--i >= 0 && Includes[CursorIndex].Text == Includes[Indices[i]].Text)
CursorIndex = i;
break;
}
return std::make_pair(CursorIndex, OffsetToEOL);
}
// Sorts and deduplicate a block of includes given by 'Includes' alphabetically
// adding the necessary replacement to 'Replaces'. 'Includes' must be in strict
// source order.
// #include directives with the same text will be deduplicated, and only the
// first #include in the duplicate #includes remains. If the `Cursor` is
// provided and put on a deleted #include, it will be moved to the remaining
// #include in the duplicate #includes.
static void sortCppIncludes(const FormatStyle &Style,
const SmallVectorImpl<IncludeDirective> &Includes,
ArrayRef<tooling::Range> Ranges, StringRef FileName,
tooling::Replacements &Replaces, unsigned *Cursor) {
if (!affectsRange(Ranges, Includes.front().Offset,
Includes.back().Offset + Includes.back().Text.size()))
const SmallVectorImpl<IncludeDirective> &Includes,
ArrayRef<tooling::Range> Ranges, StringRef FileName,
tooling::Replacements &Replaces, unsigned *Cursor) {
unsigned IncludesBeginOffset = Includes.front().Offset;
unsigned IncludesBlockSize = Includes.back().Offset +
Includes.back().Text.size() -
IncludesBeginOffset;
if (!affectsRange(Ranges, IncludesBeginOffset, IncludesBlockSize))
return;
SmallVector<unsigned, 16> Indices;
for (unsigned i = 0, e = Includes.size(); i != e; ++i)
@ -1241,37 +1276,39 @@ static void sortCppIncludes(const FormatStyle &Style,
return std::tie(Includes[LHSI].Category, Includes[LHSI].Filename) <
std::tie(Includes[RHSI].Category, Includes[RHSI].Filename);
});
// The index of the include on which the cursor will be put after
// sorting/deduplicating.
unsigned CursorIndex;
// The offset from cursor to the end of line.
unsigned CursorToEOLOffset;
if (Cursor)
std::tie(CursorIndex, CursorToEOLOffset) =
FindCursorIndex(Includes, Indices, *Cursor);
// Deduplicate #includes.
Indices.erase(std::unique(Indices.begin(), Indices.end(),
[&](unsigned LHSI, unsigned RHSI) {
return Includes[LHSI].Text == Includes[RHSI].Text;
}),
Indices.end());
// If the #includes are out of order, we generate a single replacement fixing
// the entire block. Otherwise, no replacement is generated.
if (std::is_sorted(Indices.begin(), Indices.end()))
if (Indices.size() == Includes.size() &&
std::is_sorted(Indices.begin(), Indices.end()))
return;
std::string result;
bool CursorMoved = false;
for (unsigned Index : Indices) {
if (!result.empty())
result += "\n";
result += Includes[Index].Text;
if (Cursor && !CursorMoved) {
unsigned Start = Includes[Index].Offset;
unsigned End = Start + Includes[Index].Text.size();
if (*Cursor >= Start && *Cursor < End) {
*Cursor = Includes.front().Offset + result.size() + *Cursor - End;
CursorMoved = true;
}
}
if (Cursor && CursorIndex == Index)
*Cursor = IncludesBeginOffset + result.size() - CursorToEOLOffset;
}
// 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);
auto Err = Replaces.add(tooling::Replacement(
FileName, Includes.front().Offset, result.size(), result));
FileName, Includes.front().Offset, IncludesBlockSize, result));
// FIXME: better error handling. For now, just skip the replacement for the
// release version.
if (Err)

View File

@ -0,0 +1,14 @@
// RUN: grep -Ev "// *[A-Z-]+:" %s \
// RUN: | clang-format -style="{BasedOnStyle: LLVM, SortIncludes: true}" -lines=1:5 \
// RUN: | FileCheck -strict-whitespace %s
// CHECK: {{^#include\ <a>$}}
#include <a>
// CHECK: {{^#include\ <b>$}}
#include <b>
#include <a>
#include <b>
#include <b>
{
// CHECK: {{^\ \ int x\ \ ;$}}
int x ;
}

View File

@ -260,9 +260,8 @@ static bool format(StringRef FileName) {
llvm::errs() << llvm::toString(ChangedCode.takeError()) << "\n";
return true;
}
for (const auto &R : Replaces)
Ranges.push_back({R.getOffset(), R.getLength()});
// Get new affected ranges after sorting `#includes`.
Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges);
bool IncompleteFormat = false;
Replacements FormatChanges = reformat(FormatStyle, *ChangedCode, Ranges,
AssumedFileName, &IncompleteFormat);

View File

@ -26,8 +26,9 @@ protected:
std::string sort(StringRef Code, StringRef FileName = "input.cpp") {
auto Ranges = GetCodeRange(Code);
auto Sorted =
applyAllReplacements(Code, sortIncludes(Style, Code, Ranges, FileName));
auto Replaces = sortIncludes(Style, Code, Ranges, FileName);
Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges);
auto Sorted = applyAllReplacements(Code, Replaces);
EXPECT_TRUE(static_cast<bool>(Sorted));
auto Result = applyAllReplacements(
*Sorted, reformat(Style, *Sorted, Ranges, FileName));
@ -57,10 +58,10 @@ TEST_F(SortIncludesTest, NoReplacementsForValidIncludes) {
// Identical #includes have led to a failure with an unstable sort.
std::string Code = "#include <a>\n"
"#include <b>\n"
"#include <b>\n"
"#include <b>\n"
"#include <b>\n"
"#include <c>\n";
"#include <c>\n"
"#include <d>\n"
"#include <e>\n"
"#include <f>\n";
EXPECT_TRUE(sortIncludes(Style, Code, GetCodeRange(Code), "a.cc").empty());
}
@ -286,6 +287,82 @@ TEST_F(SortIncludesTest, CalculatesCorrectCursorPosition) {
EXPECT_EQ(10u, newCursor(Code, 43));
}
TEST_F(SortIncludesTest, DeduplicateIncludes) {
EXPECT_EQ("#include <a>\n"
"#include <b>\n"
"#include <c>\n",
sort("#include <a>\n"
"#include <b>\n"
"#include <b>\n"
"#include <b>\n"
"#include <b>\n"
"#include <c>\n"));
}
TEST_F(SortIncludesTest, SortAndDeduplicateIncludes) {
EXPECT_EQ("#include <a>\n"
"#include <b>\n"
"#include <c>\n",
sort("#include <b>\n"
"#include <a>\n"
"#include <b>\n"
"#include <b>\n"
"#include <c>\n"
"#include <b>\n"));
}
TEST_F(SortIncludesTest, CalculatesCorrectCursorPositionAfterDeduplicate) {
std::string Code = "#include <b>\n" // Start of line: 0
"#include <a>\n" // Start of line: 13
"#include <b>\n" // Start of line: 26
"#include <b>\n" // Start of line: 39
"#include <c>\n" // Start of line: 52
"#include <b>\n"; // Start of line: 65
std::string Expected = "#include <a>\n" // Start of line: 0
"#include <b>\n" // Start of line: 13
"#include <c>\n"; // Start of line: 26
EXPECT_EQ(Expected, sort(Code));
// Cursor on 'i' in "#include <a>".
EXPECT_EQ(1u, newCursor(Code, 14));
// Cursor on 'b' in "#include <b>".
EXPECT_EQ(23u, newCursor(Code, 10));
EXPECT_EQ(23u, newCursor(Code, 36));
EXPECT_EQ(23u, newCursor(Code, 49));
EXPECT_EQ(23u, newCursor(Code, 36));
EXPECT_EQ(23u, newCursor(Code, 75));
// Cursor on '#' in "#include <c>".
EXPECT_EQ(26u, newCursor(Code, 52));
}
TEST_F(SortIncludesTest, DeduplicateLocallyInEachBlock) {
EXPECT_EQ("#include <a>\n"
"#include <b>\n"
"\n"
"#include <b>\n"
"#include <c>\n",
sort("#include <a>\n"
"#include <b>\n"
"\n"
"#include <c>\n"
"#include <b>\n"
"#include <b>\n"));
}
TEST_F(SortIncludesTest, ValidAffactedRangesAfterDeduplicatingIncludes) {
std::string Code = "#include <a>\n"
"#include <b>\n"
"#include <a>\n"
"#include <a>\n"
"\n"
" int x ;";
std::vector<tooling::Range> Ranges = {tooling::Range(0, 52)};
auto Replaces = sortIncludes(Style, Code, Ranges, "input.cpp");
Ranges = tooling::calculateRangesAfterReplacements(Replaces, Ranges);
EXPECT_EQ(1u, Ranges.size());
EXPECT_EQ(0u, Ranges[0].getOffset());
EXPECT_EQ(26u, Ranges[0].getLength());
}
} // end namespace
} // end namespace format
} // end namespace clang