Index: include/clang/Format/Format.h =================================================================== --- include/clang/Format/Format.h +++ include/clang/Format/Format.h @@ -546,6 +546,10 @@ /// \brief If ``true``, clang-format will sort ``#includes``. bool SortIncludes; + /// \brief If ``true``, clang-format will remove duplicate ``#includes`` when + /// sorting ``#includes``. + bool DeduplicateIncludes; + /// \brief If ``true``, a space may be inserted after C style casts. bool SpaceAfterCStyleCast; Index: lib/Format/Format.cpp =================================================================== --- lib/Format/Format.cpp +++ lib/Format/Format.cpp @@ -337,6 +337,7 @@ IO.mapOptional("PointerAlignment", Style.PointerAlignment); IO.mapOptional("ReflowComments", Style.ReflowComments); IO.mapOptional("SortIncludes", Style.SortIncludes); + IO.mapOptional("DeduplicateIncludes", Style.DeduplicateIncludes); IO.mapOptional("SpaceAfterCStyleCast", Style.SpaceAfterCStyleCast); IO.mapOptional("SpaceBeforeAssignmentOperators", Style.SpaceBeforeAssignmentOperators); @@ -1238,37 +1239,67 @@ 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 is currently put. + unsigned CurrentCursorIndex = UINT_MAX; + // The index of the include on which the cursor will be put after + // deduplicating. + unsigned CursorIndex = UINT_MAX; + if (Cursor) + // Find `CurrentCursorIndex` and `CursorIndex`. + 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) { + CurrentCursorIndex = Indices[i]; + CursorIndex = CurrentCursorIndex; + if (Style.DeduplicateIncludes) { + // 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; + } + } + + if (Style.DeduplicateIncludes) + 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) { + unsigned CursorToEOLOffset = Includes[CurrentCursorIndex].Offset + + Includes[CurrentCursorIndex].Text.size() - + *Cursor; + *Cursor = Includes.front().Offset + 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); + // If duplicate #includes are not deleted, sorting #includes shouldn't change + // their total number of characters. + unsigned num_chars_replaced = Includes.back().Offset + + Includes.back().Text.size() - + Includes.front().Offset; + assert(Style.DeduplicateIncludes || result.size() == num_chars_replaced); auto Err = Replaces.add(tooling::Replacement( - FileName, Includes.front().Offset, result.size(), result)); + FileName, Includes.front().Offset, num_chars_replaced, result)); // FIXME: better error handling. For now, just skip the replacement for the // release version. if (Err) Index: test/Format/remove-duplicate-includes.cpp =================================================================== --- /dev/null +++ test/Format/remove-duplicate-includes.cpp @@ -0,0 +1,13 @@ +// RUN: grep -Ev "// *[A-Z-]+:" %s \ +// RUN: | clang-format -style="{BasedOnStyle: LLVM, SortIncludes: true, DeduplicateIncludes:true}" -lines=1:5 \ +// RUN: | FileCheck -strict-whitespace %s +// CHECK: {{^#include\ $}} +#include +// CHECK: {{^#include\ $}} +#include +#include +#include +#include +{ + int x ; +} Index: tools/clang-format/ClangFormat.cpp =================================================================== --- tools/clang-format/ClangFormat.cpp +++ tools/clang-format/ClangFormat.cpp @@ -260,9 +260,8 @@ 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); Index: unittests/Format/SortIncludesTest.cpp =================================================================== --- unittests/Format/SortIncludesTest.cpp +++ unittests/Format/SortIncludesTest.cpp @@ -26,8 +26,9 @@ 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(Sorted)); auto Result = applyAllReplacements( *Sorted, reformat(Style, *Sorted, Ranges, FileName)); @@ -286,6 +287,87 @@ EXPECT_EQ(10u, newCursor(Code, 43)); } +TEST_F(SortIncludesTest, DeduplicateIncludes) { + Style.DeduplicateIncludes = true; + EXPECT_EQ("#include \n" + "#include \n" + "#include \n", + sort("#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n")); +} + +TEST_F(SortIncludesTest, SortAndDeduplicateIncludes) { + Style.DeduplicateIncludes = true; + EXPECT_EQ("#include \n" + "#include \n" + "#include \n", + sort("#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n" + "#include \n")); +} + +TEST_F(SortIncludesTest, CalculatesCorrectCursorPositionAfterDeduplicate) { + Style.DeduplicateIncludes = true; + std::string Code = "#include \n" // Start of line: 0 + "#include \n" // Start of line: 13 + "#include \n" // Start of line: 26 + "#include \n" // Start of line: 39 + "#include \n" // Start of line: 52 + "#include \n"; // Start of line: 65 + std::string Expected = "#include \n" // Start of line: 0 + "#include \n" // Start of line: 13 + "#include \n"; // Start of line: 26 + EXPECT_EQ(Expected, sort(Code)); + // Cursor on 'i' in "#include ". + EXPECT_EQ(1u, newCursor(Code, 14)); + // Cursor on 'b' in "#include ". + 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 ". + EXPECT_EQ(26u, newCursor(Code, 52)); +} + +TEST_F(SortIncludesTest, DeduplicateLocallyInEachBlock) { + Style.DeduplicateIncludes = true; + EXPECT_EQ("#include \n" + "#include \n" + "\n" + "#include \n" + "#include \n", + sort("#include \n" + "#include \n" + "\n" + "#include \n" + "#include \n" + "#include \n")); +} + +TEST_F(SortIncludesTest, ValidAffactedRangesAfterDeduplicatingIncludes) { + Style.DeduplicateIncludes = true; + std::string Code = "#include \n" + "#include \n" + "#include \n" + "#include \n" + "\n" + " int x ;"; + std::vector 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