https://github.com/erbsland-dev created https://github.com/llvm/llvm-project/pull/183960
Resolves #183802. Introduce a new optional `EmptyLines` key for entries in `IncludeCategories`, allowing configuration of the number of empty lines inserted between include groups. Add tests to validate the new behavior and update the documentation. >From 52c0b4524e90df56f310d75c1c4ea2d64df3f53b Mon Sep 17 00:00:00 2001 From: ErbslandDEV <[email protected]> Date: Sat, 28 Feb 2026 16:58:39 +0100 Subject: [PATCH] [clang-format] Support variable empty lines between include categories Resolves #183802. Introduce a new optional `EmptyLines` key for entries in `IncludeCategories`, allowing configuration of the number of empty lines inserted between include groups. Add tests to validate the new behavior and update the documentation. --- clang/docs/ClangFormatStyleOptions.rst | 13 +++ .../clang/Tooling/Inclusions/IncludeStyle.h | 12 +++ clang/lib/Format/Format.cpp | 42 ++++++-- clang/lib/Tooling/Inclusions/IncludeStyle.cpp | 1 + clang/unittests/Format/ConfigParseTest.cpp | 9 ++ clang/unittests/Format/SortIncludesTest.cpp | 100 +++++++++++++----- 6 files changed, 139 insertions(+), 38 deletions(-) diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index ed4e2aaa26052..0615c0a8aeda0 100644 --- a/clang/docs/ClangFormatStyleOptions.rst +++ b/clang/docs/ClangFormatStyleOptions.rst @@ -4417,6 +4417,18 @@ the configuration (without a prefix: ``Auto``). Each regular expression can be marked as case sensitive with the field ``CaseSensitive``, per default it is not. + There is a fourth and optional field ``EmptyLines`` that defines how many + empty lines are inserted before this category when ``IncludeBlocks`` is + ``IBS_Regroup``. The default is ``1``. ``EmptyLines: 0`` can be used to + suppress separation. + + When regrouping jumps over categories that are not present in the file, + clang-format uses the maximum ``EmptyLines`` value of all category + priorities between the previous and the next emitted category. + + ``MaxEmptyLinesToKeep`` still applies to the final number of consecutive + empty lines kept in the formatted output. + To configure this in the .clang-format file, use: .. code-block:: yaml @@ -4430,6 +4442,7 @@ the configuration (without a prefix: ``Auto``). Priority: 3 - Regex: '<[[:alnum:].]+>' Priority: 4 + EmptyLines: 2 - Regex: '.*' Priority: 1 SortPriority: 0 diff --git a/clang/include/clang/Tooling/Inclusions/IncludeStyle.h b/clang/include/clang/Tooling/Inclusions/IncludeStyle.h index bf060617deec7..ad091994432a7 100644 --- a/clang/include/clang/Tooling/Inclusions/IncludeStyle.h +++ b/clang/include/clang/Tooling/Inclusions/IncludeStyle.h @@ -63,8 +63,11 @@ struct IncludeStyle { int SortPriority; /// If the regular expression is case sensitive. bool RegexIsCaseSensitive; + /// The number of blank lines to add *before* this category. + int EmptyLines = 1; bool operator==(const IncludeCategory &Other) const { return Regex == Other.Regex && Priority == Other.Priority && + EmptyLines == Other.EmptyLines && RegexIsCaseSensitive == Other.RegexIsCaseSensitive; } }; @@ -99,6 +102,14 @@ struct IncludeStyle { /// Each regular expression can be marked as case sensitive with the field /// ``CaseSensitive``, per default it is not. /// + /// There is a fourth and optional field ``EmptyLines`` that defines how + /// many empty lines are inserted before this category when + /// ``IncludeBlocks = IBS_Regroup``. The default is ``1``. + /// + /// If regrouping jumps over categories that are not present in the file, + /// the maximum ``EmptyLines`` value of all category priorities between the + /// two emitted categories is used. + /// /// To configure this in the .clang-format file, use: /// \code{.yaml} /// IncludeCategories: @@ -110,6 +121,7 @@ struct IncludeStyle { /// Priority: 3 /// - Regex: '<[[:alnum:].]+>' /// Priority: 4 + /// EmptyLines: 2 /// - Regex: '.*' /// Priority: 1 /// SortPriority: 0 diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp index 2f67ec86b101a..c4fd8d8246e66 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -1808,8 +1808,8 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) { LLVMStyle.IfMacros.push_back("KJ_IF_MAYBE"); LLVMStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Preserve; LLVMStyle.IncludeStyle.IncludeCategories = { - {"^\"(llvm|llvm-c|clang|clang-c)/", 2, 0, false}, - {"^(<|\"(gtest|gmock|isl|json)/)", 3, 0, false}, + {"^\"(llvm|llvm-c|clang|clang-c)/", 2, 0, false, 1}, + {"^(<|\"(gtest|gmock|isl|json)/)", 3, 0, false, 1}, {".*", 1, 0, false}}; LLVMStyle.IncludeStyle.IncludeIsMainRegex = "(Test)?$"; LLVMStyle.IncludeStyle.MainIncludeChar = tooling::IncludeStyle::MICD_Quote; @@ -1966,10 +1966,11 @@ FormatStyle getGoogleStyle(FormatStyle::LanguageKind Language) { GoogleStyle.AttributeMacros.push_back("absl_nullability_unknown"); GoogleStyle.BreakTemplateDeclarations = FormatStyle::BTDS_Yes; GoogleStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; - GoogleStyle.IncludeStyle.IncludeCategories = {{"^<ext/.*\\.h>", 2, 0, false}, - {"^<.*\\.h>", 1, 0, false}, - {"^<.*", 2, 0, false}, - {".*", 3, 0, false}}; + GoogleStyle.IncludeStyle.IncludeCategories = { + {"^<ext/.*\\.h>", 2, 0, false, 1}, + {"^<.*\\.h>", 1, 0, false, 1}, + {"^<.*", 2, 0, false, 1}, + {".*", 3, 0, false, 1}}; GoogleStyle.IncludeStyle.IncludeIsMainRegex = "([-_](test|unittest))?$"; GoogleStyle.IndentCaseLabels = true; GoogleStyle.KeepEmptyLines.AtStartOfBlock = false; @@ -3535,6 +3536,25 @@ static void sortCppIncludes(const FormatStyle &Style, }), Indices.end()); + const auto GetEmptyLines = [&](int LastCategory, int NewCategory) -> int { + if (LastCategory == NewCategory) { + return 0; + } + const int LowerBound = std::min(LastCategory, NewCategory); + const int UpperBound = std::max(LastCategory, NewCategory); + int EmptyLines = 1; + for (const auto &IncludeCategory : Style.IncludeStyle.IncludeCategories) { + if (IncludeCategory.Priority <= LowerBound) { + continue; + } + if (IncludeCategory.Priority > UpperBound) { + break; + } + EmptyLines = std::max(EmptyLines, IncludeCategory.EmptyLines); + } + return EmptyLines; + }; + int CurrentCategory = Includes.front().Category; // If the #includes are out of order, we generate a single replacement fixing @@ -3551,18 +3571,22 @@ static void sortCppIncludes(const FormatStyle &Style, const auto OldCursor = Cursor ? *Cursor : 0; std::string result; for (unsigned Index : Indices) { + const auto NewCategory = Includes[Index].Category; if (!result.empty()) { result += "\n"; if (Style.IncludeStyle.IncludeBlocks == tooling::IncludeStyle::IBS_Regroup && - CurrentCategory != Includes[Index].Category) { - result += "\n"; + CurrentCategory != NewCategory) { + const int EmptyLineCount = GetEmptyLines(CurrentCategory, NewCategory); + for (int Count = 0; Count < EmptyLineCount; ++Count) { + result += "\n"; + } } } result += Includes[Index].Text; if (Cursor && CursorIndex == Index) *Cursor = IncludesBeginOffset + result.size() - CursorToEOLOffset; - CurrentCategory = Includes[Index].Category; + CurrentCategory = NewCategory; } if (Cursor && *Cursor >= IncludesEndOffset) diff --git a/clang/lib/Tooling/Inclusions/IncludeStyle.cpp b/clang/lib/Tooling/Inclusions/IncludeStyle.cpp index 05dfb50589de0..493475382ac4f 100644 --- a/clang/lib/Tooling/Inclusions/IncludeStyle.cpp +++ b/clang/lib/Tooling/Inclusions/IncludeStyle.cpp @@ -19,6 +19,7 @@ void MappingTraits<IncludeStyle::IncludeCategory>::mapping( IO.mapOptional("Priority", Category.Priority); IO.mapOptional("SortPriority", Category.SortPriority); IO.mapOptional("CaseSensitive", Category.RegexIsCaseSensitive); + IO.mapOptional("EmptyLines", Category.EmptyLines, 1); } void ScalarEnumerationTraits<IncludeStyle::IncludeBlocksStyle>::enumeration( diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index f270602f32604..17a33fc6e487d 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -1054,6 +1054,15 @@ TEST(ConfigParseTest, ParsesConfiguration) { " Priority: 1\n" " CaseSensitive: true", IncludeStyle.IncludeCategories, ExpectedCategories); + ExpectedCategories = {{"abc/.*", 2, 0, false, 2}, {".*", 1, 0, true, 1}}; + CHECK_PARSE("IncludeCategories:\n" + " - Regex: abc/.*\n" + " Priority: 2\n" + " EmptyLines: 2\n" + " - Regex: .*\n" + " Priority: 1\n" + " CaseSensitive: true", + IncludeStyle.IncludeCategories, ExpectedCategories); CHECK_PARSE("IncludeIsMainRegex: 'abc$'", IncludeStyle.IncludeIsMainRegex, "abc$"); CHECK_PARSE("IncludeIsMainSourceRegex: 'abc$'", diff --git a/clang/unittests/Format/SortIncludesTest.cpp b/clang/unittests/Format/SortIncludesTest.cpp index 48ecd5d32d034..0b0408e172bbb 100644 --- a/clang/unittests/Format/SortIncludesTest.cpp +++ b/clang/unittests/Format/SortIncludesTest.cpp @@ -87,19 +87,19 @@ TEST_F(SortIncludesTest, TrailingComments) { TEST_F(SortIncludesTest, SortedIncludesUsingSortPriorityAttribute) { FmtStyle.IncludeStyle.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; FmtStyle.IncludeStyle.IncludeCategories = { - {"^<sys/param\\.h>", 1, 0, false}, - {"^<sys/types\\.h>", 1, 1, false}, - {"^<sys.*/", 1, 2, false}, - {"^<uvm/", 2, 3, false}, - {"^<machine/", 3, 4, false}, - {"^<dev/", 4, 5, false}, - {"^<net.*/", 5, 6, false}, - {"^<protocols/", 5, 7, false}, - {"^<(fs|miscfs|msdosfs|nfs|ntfs|ufs)/", 6, 8, false}, - {"^<(x86|amd64|i386|xen)/", 7, 8, false}, - {"<path", 9, 11, false}, - {"^<[^/].*\\.h>", 8, 10, false}, - {"^\".*\\.h\"", 10, 12, false}}; + {"^<sys/param\\.h>", 1, 0, false, 1}, + {"^<sys/types\\.h>", 1, 1, false, 1}, + {"^<sys.*/", 1, 2, false, 1}, + {"^<uvm/", 2, 3, false, 1}, + {"^<machine/", 3, 4, false, 1}, + {"^<dev/", 4, 5, false, 1}, + {"^<net.*/", 5, 6, false, 1}, + {"^<protocols/", 5, 7, false, 1}, + {"^<(fs|miscfs|msdosfs|nfs|ntfs|ufs)/", 6, 8, false, 1}, + {"^<(x86|amd64|i386|xen)/", 7, 8, false, 1}, + {"<path", 9, 11, false, 1}, + {"^<[^/].*\\.h>", 8, 10, false, 1}, + {"^\".*\\.h\"", 10, 12, false, 1}}; verifyFormat("#include <sys/param.h>\n" "#include <sys/types.h>\n" "#include <sys/ioctl.h>\n" @@ -627,6 +627,48 @@ TEST_F(SortIncludesTest, MainHeaderIsSeparatedWhenRegroupping) { "a.cc")); } +TEST_F(SortIncludesTest, EmptyLinesUseMaxAcrossSkippedCategories) { + Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; + FmtStyle.MaxEmptyLinesToKeep = 2; + Style.IncludeCategories = { + {"^\"b", 1, 0, false, 2}, + {"^\"c", 2, 0, false, 1}, + {"^\"d", 3, 0, false, 2}, + {"^\"e", 4, 0, false, 1}, + }; + + verifyFormat("#include \"input.h\"\n" + "\n" + "\n" + "#include \"c.h\"\n" + "\n" + "\n" + "#include \"e.h\"\n", + sort("#include \"e.h\"\n" + "#include \"c.h\"\n" + "#include \"input.h\"\n", + "input.cpp"), + FmtStyle); +} + +TEST_F(SortIncludesTest, EmptyLinesCanSeparateMainHeaderByTwoBlankLines) { + Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; + FmtStyle.MaxEmptyLinesToKeep = 2; + Style.IncludeCategories = { + {"^\"project/.*\"", 1, 0, false, 2}, + {".*", 2, 0, false, 1}, + }; + + verifyFormat("#include \"input.h\"\n" + "\n" + "\n" + "#include \"project/detail.h\"\n", + sort("#include \"project/detail.h\"\n" + "#include \"input.h\"\n", + "input.cpp"), + FmtStyle); +} + TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveSorting) { FmtStyle.SortIncludes.IgnoreCase = true; @@ -644,7 +686,7 @@ TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveSorting) { Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; Style.IncludeCategories = { - {"^\"", 1, 0, false}, {"^<.*\\.h>$", 2, 0, false}, {"^<", 3, 0, false}}; + {"^\"", 1, 0, false, 1}, {"^<.*\\.h>$", 2, 0, false}, {"^<", 3, 0, false, 1}}; StringRef UnsortedCode = "#include \"qt.h\"\n" "#include <algorithm>\n" @@ -693,11 +735,11 @@ TEST_F(SortIncludesTest, SupportCaseInsensitiveMatching) { TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveMachting) { Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; - Style.IncludeCategories = {{"^\"", 1, 0, false}, - {"^<.*\\.h>$", 2, 0, false}, - {"^<Q[A-Z][^\\.]*>", 3, 0, false}, - {"^<Qt[^\\.]*>", 4, 0, false}, - {"^<", 5, 0, false}}; + Style.IncludeCategories = {{"^\"", 1, 0, false, 1}, + {"^<.*\\.h>$", 2, 0, false, 1}, + {"^<Q[A-Z][^\\.]*>", 3, 0, false, 1}, + {"^<Qt[^\\.]*>", 4, 0, false, 1}, + {"^<", 5, 0, false, 1}}; StringRef UnsortedCode = "#include <QWidget>\n" "#include \"qt.h\"\n" @@ -742,8 +784,8 @@ TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveMachting) { } TEST_F(SortIncludesTest, NegativePriorities) { - Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false}, - {".*", 1, 0, false}}; + Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false, 1}, + {".*", 1, 0, false, 1}}; verifyFormat("#include \"important_os_header.h\"\n" "#include \"c_main.h\"\n" "#include \"a_other.h\"", @@ -763,8 +805,8 @@ TEST_F(SortIncludesTest, NegativePriorities) { } TEST_F(SortIncludesTest, PriorityGroupsAreSeparatedWhenRegroupping) { - Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false}, - {".*", 1, 0, false}}; + Style.IncludeCategories = {{".*important_os_header.*", -1, 0, false, 1}, + {".*", 1, 0, false, 1}}; Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; verifyFormat("#include \"important_os_header.h\"\n" @@ -824,7 +866,7 @@ TEST_F(SortIncludesTest, Style.IncludeBlocks = Style.IBS_Regroup; FmtStyle.LineEnding = FormatStyle::LE_CRLF; Style.IncludeCategories = { - {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}}; + {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false, 1}}; StringRef Code = "#include \"a\"\r\n" // Start of line: 0 "\r\n" // Start of line: 14 "#include \"b\"\r\n" // Start of line: 16 @@ -847,7 +889,7 @@ TEST_F( CalculatesCorrectCursorPositionWhenRemoveLinesReplacementsWithRegroupingAndCRLF) { Style.IncludeBlocks = Style.IBS_Regroup; FmtStyle.LineEnding = FormatStyle::LE_CRLF; - Style.IncludeCategories = {{".*", 0, 0, false}}; + Style.IncludeCategories = {{".*", 0, 0, false, 1}}; StringRef Code = "#include \"a\"\r\n" // Start of line: 0 "\r\n" // Start of line: 14 "#include \"b\"\r\n" // Start of line: 16 @@ -882,7 +924,7 @@ TEST_F( Style.IncludeBlocks = Style.IBS_Regroup; FmtStyle.LineEnding = FormatStyle::LE_CRLF; Style.IncludeCategories = { - {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}}; + {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false, 1}, {".*", 2, 2, false, 1}}; StringRef Code = "#include \"a\"\r\n" // Start of line: 0 "#include \"b\"\r\n" // Start of line: 14 "#include \"c\"\r\n" // Start of line: 28 @@ -909,7 +951,7 @@ TEST_F( Style.IncludeBlocks = Style.IBS_Regroup; FmtStyle.LineEnding = FormatStyle::LE_CRLF; Style.IncludeCategories = { - {"^\"a\"", 0, 0, false}, {"^\"b\"", 1, 1, false}, {".*", 2, 2, false}}; + {"^\"a\"", 0, 0, false, 1}, {"^\"b\"", 1, 1, false, 1}, {".*", 2, 2, false, 1}}; StringRef Code = "#include \"a\"\r\n" // Start of line: 0 "\r\n" // Start of line: 14 "#include \"c\"\r\n" // Start of line: 16 @@ -1157,7 +1199,7 @@ TEST_F(SortIncludesTest, MainIncludeCharAnyPickAngleBracket) { TEST_F(SortIncludesTest, MainIncludeCharQuoteAndRegroup) { Style.IncludeCategories = { - {"lib-a", 1, 0, false}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false}}; + {"lib-a", 1, 0, false, 1}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false, 1}}; Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; Style.MainIncludeChar = tooling::IncludeStyle::MICD_Quote; @@ -1187,7 +1229,7 @@ TEST_F(SortIncludesTest, MainIncludeCharQuoteAndRegroup) { TEST_F(SortIncludesTest, MainIncludeCharAngleBracketAndRegroup) { Style.IncludeCategories = { - {"lib-a", 1, 0, false}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false}}; + {"lib-a", 1, 0, false, 1}, {"lib-b", 2, 0, false}, {"lib-c", 3, 0, false, 1}}; Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; Style.MainIncludeChar = tooling::IncludeStyle::MICD_AngleBracket; _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
