[clang] [clang-format] Add FilesBeforeFolders option to SortIncludes (PR #208954)
Savvas Shiakas via cfe-commits
cfe-commits at lists.llvm.org
Fri Aug 28 01:55:08 PDT 2026
https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From be33e64dabc26c6c9f2f67aad8180fad3338cffa Mon Sep 17 00:00:00 2001
From: Shiakaron <savvas.shiakas at gmail.com>
Date: Sat, 11 Jul 2026 22:50:12 +0100
Subject: [PATCH] [clang-format] Add FilesBeforeFolders option to SortIncludes
Add a new `SortIncludes.FilesBeforeFolders` boolean option that, when
enabled, sorts includes so that files in a directory appear before
subdirectories at each level, recursively. Within a level, files and
subdirectories are each sorted alphabetically.
For example, with FilesBeforeFolders: true:
#include "x.h"
#include "y.h"
#include "z.h"
#include "bar/g.h"
#include "bar/h.h"
#include "bar/i.h"
#include "bar/alpha/e.h"
#include "bar/alpha/f.h"
#include "bar/beta/d.h"
#include "foo/a.h"
Assisted by: Github Copilot CLI - Claude Sonnet 4.6
---
clang/docs/ClangFormatStyleOptions.md | 22 +++
clang/include/clang/Format/Format.h | 25 +++-
clang/lib/Format/Format.cpp | 48 +++++-
clang/unittests/Format/ConfigParseTest.cpp | 13 +-
clang/unittests/Format/SortIncludesTest.cpp | 158 ++++++++++++++++++++
5 files changed, 255 insertions(+), 11 deletions(-)
diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md
index ee239f768cf2b..0f4fcb6a42a44 100644
--- a/clang/docs/ClangFormatStyleOptions.md
+++ b/clang/docs/ClangFormatStyleOptions.md
@@ -6989,6 +6989,28 @@ the configuration (without a prefix: `Auto`).
#include "A10.h" #include "A2.h"
```
+ - ``bool FilesBeforeFolders`` When ``true``, sort includes so that files in a directory appear
+ before subdirectories at each level, recursively. Within a level,
+ files and folders are each sorted alphabetically.
+ When ``false`` (default), sorts includes purely alphabetically.
+
+ This option is a secondary sort key within each ``Priority`` group
+ defined by ``IncludeCategories``. Includes in different ``Priority``
+ groups are still separated by that primary ordering.
+
+ ```c++
+ true: false (default):
+ #include "x.h" vs. #include "bar/alpha/e.h"
+ #include "y.h" #include "bar/alpha/f.h"
+ #include "z.h" #include "bar/beta/d.h"
+ #include "bar/g.h" #include "bar/g.h"
+ #include "bar/h.h" #include "bar/h.h"
+ #include "bar/i.h" #include "bar/i.h"
+ #include "bar/alpha/e.h" #include "foo/a.h"
+ #include "bar/alpha/f.h" #include "x.h"
+ #include "bar/beta/d.h" #include "y.h"
+ #include "foo/a.h" #include "z.h"
+ ``
(sortjavastaticimport)=
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 0f91df5b1b32d..ddef3659af65b 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -5106,9 +5106,32 @@ struct FormatStyle {
/// #include "A10.h" #include "A2.h"
/// \endcode
bool Natural;
+ /// When ``true``, sort includes so that files in a directory appear
+ /// before subdirectories at each level, recursively. Within a level,
+ /// files and folders are each sorted alphabetically.
+ /// When ``false`` (default), sorts includes purely alphabetically.
+ ///
+ /// This option is a secondary sort key within each ``Priority`` group
+ /// defined by ``IncludeCategories``. Includes in different ``Priority``
+ /// groups are still separated by that primary ordering.
+ /// \code
+ /// true: false (default):
+ /// #include "x.h" vs. #include "bar/alpha/e.h"
+ /// #include "y.h" #include "bar/alpha/f.h"
+ /// #include "z.h" #include "bar/beta/d.h"
+ /// #include "bar/g.h" #include "bar/g.h"
+ /// #include "bar/h.h" #include "bar/h.h"
+ /// #include "bar/i.h" #include "bar/i.h"
+ /// #include "bar/alpha/e.h" #include "foo/a.h"
+ /// #include "bar/alpha/f.h" #include "x.h"
+ /// #include "bar/beta/d.h" #include "y.h"
+ /// #include "foo/a.h" #include "z.h"
+ /// \endcode
+ bool FilesBeforeFolders;
bool operator==(const SortIncludesOptions &R) const {
return Enabled == R.Enabled && IgnoreCase == R.IgnoreCase &&
- IgnoreExtension == R.IgnoreExtension && Natural == R.Natural;
+ IgnoreExtension == R.IgnoreExtension && Natural == R.Natural &&
+ FilesBeforeFolders == R.FilesBeforeFolders;
}
bool operator!=(const SortIncludesOptions &R) const {
return !(*this == R);
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 574014f360354..d1798df7333c5 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -849,17 +849,20 @@ template <> struct MappingTraits<FormatStyle::SortIncludesOptions> {
FormatStyle::SortIncludesOptions{/*Enabled=*/true,
/*IgnoreCase=*/true,
/*IgnoreExtension=*/false,
- /*Natural=*/false});
+ /*Natural=*/false,
+ /*FilesBeforeFolders=*/false});
IO.enumCase(Value, "CaseSensitive",
FormatStyle::SortIncludesOptions{/*Enabled=*/true,
/*IgnoreCase=*/false,
/*IgnoreExtension=*/false,
- /*Natural=*/false});
+ /*Natural=*/false,
+ /*FilesBeforeFolders=*/false});
IO.enumCase(Value, "Natural",
FormatStyle::SortIncludesOptions{/*Enabled=*/true,
/*IgnoreCase=*/false,
/*IgnoreExtension=*/false,
- /*Natural=*/true});
+ /*Natural=*/true,
+ /*FilesBeforeFolders=*/false});
// For backward compatibility.
IO.enumCase(Value, "false", FormatStyle::SortIncludesOptions{});
@@ -867,14 +870,15 @@ template <> struct MappingTraits<FormatStyle::SortIncludesOptions> {
FormatStyle::SortIncludesOptions{/*Enabled=*/true,
/*IgnoreCase=*/false,
/*IgnoreExtension=*/false,
- /*Natural=*/false});
+ /*Natural=*/false,
+ /*FilesBeforeFolders=*/false});
}
-
static void mapping(IO &IO, FormatStyle::SortIncludesOptions &Value) {
IO.mapOptional("Enabled", Value.Enabled);
IO.mapOptional("IgnoreCase", Value.IgnoreCase);
IO.mapOptional("IgnoreExtension", Value.IgnoreExtension);
IO.mapOptional("Natural", Value.Natural);
+ IO.mapOptional("FilesBeforeFolders", Value.FilesBeforeFolders);
}
};
@@ -2023,7 +2027,8 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
LLVMStyle.ShortNamespaceLines = 1;
LLVMStyle.SkipMacroDefinitionBody = false;
LLVMStyle.SortIncludes = {/*Enabled=*/true, /*IgnoreCase=*/false,
- /*IgnoreExtension=*/false, /*Natural=*/false};
+ /*IgnoreExtension=*/false, /*Natural=*/false,
+ /*FilesBeforeFolders=*/false};
LLVMStyle.SortJavaStaticImport = FormatStyle::SJSIO_Before;
LLVMStyle.SortUsingDeclarations = FormatStyle::SUD_LexicographicNumeric;
LLVMStyle.SpaceAfterCStyleCast = false;
@@ -3691,6 +3696,37 @@ static void sortCppIncludes(const FormatStyle &Style,
? &StringRef::compare_numeric
: &StringRef::compare;
+ while (Style.SortIncludes.FilesBeforeFolders) {
+ auto [LHead, LTail] = LHSStem.split('/');
+ auto [RHead, RTail] = RHSStem.split('/');
+
+ bool LIsFile = LTail.empty();
+ bool RIsFile = RTail.empty();
+
+ // A file at this level sorts before a subdirectory at this level.
+ if (LIsFile != RIsFile)
+ return LIsFile;
+
+ // Both are leaf filenames — they are equal at this level, so we
+ // can break out of the loop and compare the full filenames later.
+ if (LIsFile)
+ break;
+
+ // Both are directory components at this level — compare them.
+ if (Style.SortIncludes.IgnoreCase) {
+ int Cmp = std::invoke(Compare, StringRef(LHead.lower()),
+ StringRef(RHead.lower()));
+ if (Cmp != 0)
+ return Cmp < 0;
+ }
+ if (int Cmp = std::invoke(Compare, LHead, RHead); Cmp != 0)
+ return Cmp < 0;
+
+ // Directory components are equal; descend into the next level.
+ LHSStem = LTail;
+ RHSStem = RTail;
+ }
+
if (Style.SortIncludes.IgnoreCase) {
int Cmp = std::invoke(Compare, StringRef(LHSStemLower), RHSStemLower);
if (Cmp != 0)
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 53fbec9806ca4..2076e4dacfcb0 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -273,6 +273,7 @@ TEST(ConfigParseTest, ParsesConfigurationBools) {
CHECK_PARSE_NESTED_BOOL(SpacesInParensOptions, InEmptyParentheses);
CHECK_PARSE_NESTED_BOOL(SpacesInParensOptions, Other);
CHECK_PARSE_NESTED_BOOL(SortIncludes, Enabled);
+ CHECK_PARSE_NESTED_BOOL(SortIncludes, FilesBeforeFolders);
CHECK_PARSE_NESTED_BOOL(SortIncludes, IgnoreCase);
CHECK_PARSE_NESTED_BOOL(SortIncludes, IgnoreExtension);
}
@@ -1171,21 +1172,25 @@ TEST(ConfigParseTest, ParsesConfiguration) {
CHECK_PARSE("SortIncludes: true", SortIncludes,
FormatStyle::SortIncludesOptions(
{/*Enabled=*/true, /*IgnoreCase=*/false,
- /*IgnoreExtension=*/false, /*Natural=*/false}));
+ /*IgnoreExtension=*/false, /*Natural=*/false,
+ /*FilesBeforeFolders=*/false}));
CHECK_PARSE("SortIncludes: false", SortIncludes,
FormatStyle::SortIncludesOptions{});
CHECK_PARSE("SortIncludes: CaseInsensitive", SortIncludes,
FormatStyle::SortIncludesOptions(
{/*Enabled=*/true, /*IgnoreCase=*/true,
- /*IgnoreExtension=*/false, /*Natural=*/false}));
+ /*IgnoreExtension=*/false, /*Natural=*/false,
+ /*FilesBeforeFolders=*/false}));
CHECK_PARSE("SortIncludes: CaseSensitive", SortIncludes,
FormatStyle::SortIncludesOptions(
{/*Enabled=*/true, /*IgnoreCase=*/false,
- /*IgnoreExtension=*/false, /*Natural=*/false}));
+ /*IgnoreExtension=*/false, /*Natural=*/false,
+ /*FilesBeforeFolders=*/false}));
CHECK_PARSE("SortIncludes: Natural", SortIncludes,
FormatStyle::SortIncludesOptions(
{/*Enabled=*/true, /*IgnoreCase=*/false,
- /*IgnoreExtension=*/false, /*Natural=*/true}));
+ /*IgnoreExtension=*/false, /*Natural=*/true,
+ /*FilesBeforeFolders=*/false}));
CHECK_PARSE("SortIncludes: Never", SortIncludes,
FormatStyle::SortIncludesOptions{});
diff --git a/clang/unittests/Format/SortIncludesTest.cpp b/clang/unittests/Format/SortIncludesTest.cpp
index a6e9e18496f8d..fe193b7a43df2 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -1537,6 +1537,164 @@ TEST_F(SortIncludesTest, IgnoreExtension) {
"input.h"));
}
+TEST_F(SortIncludesTest, FilesBeforeFolders) {
+ // When false (default), sorting is purely alphabetical: "bar/" starts with
+ // 'b', which sorts before 'f' (foo/) and 'x'/'y'/'z', so subdirectories
+ // interleave with files based on the directory name alone.
+ //
+ // false (default): true:
+ // #include "bar/alpha/e.h" #include "x.h"
+ // #include "bar/alpha/f.h" #include "y.h"
+ // #include "bar/beta/d.h" #include "z.h"
+ // #include "bar/g.h" #include "bar/g.h"
+ // #include "bar/h.h" #include "bar/h.h"
+ // #include "bar/i.h" #include "bar/i.h"
+ // #include "foo/a.h" #include "bar/alpha/e.h"
+ // #include "x.h" #include "bar/alpha/f.h"
+ // #include "y.h" #include "bar/beta/d.h"
+ // #include "z.h" #include "foo/a.h"
+ FmtStyle.SortIncludes.FilesBeforeFolders = false;
+ verifyFormat("#include \"bar/alpha/e.h\"\n"
+ "#include \"bar/alpha/f.h\"\n"
+ "#include \"bar/beta/d.h\"\n"
+ "#include \"bar/g.h\"\n"
+ "#include \"bar/h.h\"\n"
+ "#include \"bar/i.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"x.h\"\n"
+ "#include \"y.h\"\n"
+ "#include \"z.h\"",
+ sort("#include \"z.h\"\n"
+ "#include \"bar/alpha/f.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"bar/g.h\"\n"
+ "#include \"x.h\"\n"
+ "#include \"bar/beta/d.h\"\n"
+ "#include \"bar/i.h\"\n"
+ "#include \"y.h\"\n"
+ "#include \"bar/h.h\"\n"
+ "#include \"bar/alpha/e.h\"",
+ "input.h"));
+
+ // When true, all files at the current directory level sort before any
+ // subdirectory. Within a level, files and folders are each sorted
+ // alphabetically. This applies recursively: inside "bar/", the direct
+ // files (g.h, h.h, i.h) sort before the subdirectories (alpha/, beta/).
+ FmtStyle.SortIncludes.FilesBeforeFolders = true;
+ verifyFormat("#include \"x.h\"\n"
+ "#include \"y.h\"\n"
+ "#include \"z.h\"\n"
+ "#include \"bar/g.h\"\n"
+ "#include \"bar/h.h\"\n"
+ "#include \"bar/i.h\"\n"
+ "#include \"bar/alpha/e.h\"\n"
+ "#include \"bar/alpha/f.h\"\n"
+ "#include \"bar/beta/d.h\"\n"
+ "#include \"foo/a.h\"",
+ sort("#include \"z.h\"\n"
+ "#include \"bar/alpha/f.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"bar/g.h\"\n"
+ "#include \"x.h\"\n"
+ "#include \"bar/beta/d.h\"\n"
+ "#include \"bar/i.h\"\n"
+ "#include \"y.h\"\n"
+ "#include \"bar/h.h\"\n"
+ "#include \"bar/alpha/e.h\"",
+ "input.h"));
+
+ // Recursion: files in a subdir sort before nested subdirs.
+ verifyFormat("#include \"dir/a.h\"\n"
+ "#include \"dir/b.h\"\n"
+ "#include \"dir/sub/a.h\"\n"
+ "#include \"dir/sub/b.h\"",
+ sort("#include \"dir/sub/b.h\"\n"
+ "#include \"dir/a.h\"\n"
+ "#include \"dir/sub/a.h\"\n"
+ "#include \"dir/b.h\"",
+ "input.h"));
+
+ // FilesBeforeFolders combined with IgnoreCase.
+ FmtStyle.SortIncludes.IgnoreCase = true;
+ verifyFormat("#include \"A.h\"\n"
+ "#include \"b.h\"\n"
+ "#include \"Bar/a.h\"\n"
+ "#include \"foo/B.h\"",
+ sort("#include \"foo/B.h\"\n"
+ "#include \"Bar/a.h\"\n"
+ "#include \"b.h\"\n"
+ "#include \"A.h\"",
+ "input.h"));
+ // Case-sensitive comparison is the tiebreaker when directory components are
+ // case-insensitively equal: "Bar" (0x42) < "bar" (0x62).
+ verifyFormat("#include \"Bar/a.h\"\n"
+ "#include \"bar/a.h\"",
+ sort("#include \"bar/a.h\"\n"
+ "#include \"Bar/a.h\"",
+ "input.h"));
+ FmtStyle.SortIncludes.IgnoreCase = false;
+
+ // FilesBeforeFolders combined with IgnoreExtension.
+ FmtStyle.SortIncludes.IgnoreExtension = true;
+ verifyFormat("#include \"a.h\"\n"
+ "#include \"a.inc\"\n"
+ "#include \"a-util.h\"\n"
+ "#include \"bar/a.h\"\n"
+ "#include \"bar/b.h\"",
+ sort("#include \"bar/b.h\"\n"
+ "#include \"a-util.h\"\n"
+ "#include \"bar/a.h\"\n"
+ "#include \"a.inc\"\n"
+ "#include \"a.h\"",
+ "input.h"));
+ FmtStyle.SortIncludes.IgnoreExtension = false;
+
+ // Mixing "" and <> includes in the same block: FilesBeforeFolders applies
+ // equally to both quote and angle-bracket includes, but the delimiter itself
+ // participates in the plain alphabetical comparison when the flag is false:
+ // '"' (0x22) < '<' (0x3C), so quote-delimited includes sort before
+ // angle-bracket ones regardless of path content.
+ //
+ // This example uses a quote include that is a folder path ("beta/x.hpp") and
+ // an angle-bracket include that is a root-level file (<alpha.hpp>).
+ //
+ // false (alphabetical, delimiter wins):
+ // "beta/x.hpp" sorts first because '"' < '<'
+ // true (files-before-folders, structural comparison):
+ // <alpha.hpp> sorts first because it is a root-level file while
+ // "beta/x.hpp" lives inside a subdirectory
+ FmtStyle.IncludeStyle.IncludeCategories.clear();
+ FmtStyle.SortIncludes.FilesBeforeFolders = false;
+ verifyFormat("#include \"beta/x.hpp\"\n"
+ "#include <alpha.hpp>",
+ sort("#include <alpha.hpp>\n"
+ "#include \"beta/x.hpp\"",
+ "input.h"));
+ FmtStyle.SortIncludes.FilesBeforeFolders = true;
+ verifyFormat("#include <alpha.hpp>\n"
+ "#include \"beta/x.hpp\"",
+ sort("#include \"beta/x.hpp\"\n"
+ "#include <alpha.hpp>",
+ "input.h"));
+}
+
+TEST_F(SortIncludesTest, FilesBeforeFoldersWithPriority) {
+ // FilesBeforeFolders is a secondary sort key: Priority groups stay separate
+ // and FilesBeforeFolders only reorders includes within the same group.
+ Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup;
+ Style.IncludeCategories = {{"^<", 1, 0, false}, {"^\"", 2, 0, false}};
+ FmtStyle.SortIncludes.FilesBeforeFolders = true;
+ verifyFormat("#include <stdio.h>\n"
+ "#include <sys/stat.h>\n"
+ "\n"
+ "#include \"utils.h\"\n"
+ "#include \"foo/bar.h\"",
+ sort("#include <sys/stat.h>\n"
+ "#include \"foo/bar.h\"\n"
+ "#include <stdio.h>\n"
+ "#include \"utils.h\""));
+}
+
} // end namespace
} // end namespace format
} // end namespace clang
More information about the cfe-commits
mailing list