[clang] Support variable empty lines between include categories (PR #183960)
via cfe-commits
cfe-commits at lists.llvm.org
Sat Feb 28 13:49:39 PST 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang-format
Author: Tobias Erbsland (erbsland-dev)
<details>
<summary>Changes</summary>
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.
---
Full diff: https://github.com/llvm/llvm-project/pull/183960.diff
6 Files Affected:
- (modified) clang/docs/ClangFormatStyleOptions.rst (+13)
- (modified) clang/include/clang/Tooling/Inclusions/IncludeStyle.h (+12)
- (modified) clang/lib/Format/Format.cpp (+33-9)
- (modified) clang/lib/Tooling/Inclusions/IncludeStyle.cpp (+1)
- (modified) clang/unittests/Format/ConfigParseTest.cpp (+9)
- (modified) clang/unittests/Format/SortIncludesTest.cpp (+71-29)
``````````diff
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;
``````````
</details>
https://github.com/llvm/llvm-project/pull/183960
More information about the cfe-commits
mailing list