[clang] [clang-format] Add FilesBeforeFolders option to SortIncludes (PR #208954)
Savvas Shiakas via cfe-commits
cfe-commits at lists.llvm.org
Sat Aug 1 13:44:58 PDT 2026
https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From 0b88b235798b4a9ebed8aa18bcc84d8248cc014d 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.rst | 19 +++
clang/include/clang/Format/Format.h | 21 ++-
clang/lib/Format/Format.cpp | 43 ++++++-
clang/unittests/Format/ConfigParseTest.cpp | 13 +-
clang/unittests/Format/SortIncludesTest.cpp | 134 ++++++++++++++++++++
5 files changed, 219 insertions(+), 11 deletions(-)
diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index e8cf2409e6c70..2bff6bce5c36c 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -6831,6 +6831,25 @@ the configuration (without a prefix: ``Auto``).
#include "A2.h" vs. #include "A10.h"
#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.
+
+ .. code-block:: 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 540a50047696a..6117682b06f6f 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -5083,9 +5083,28 @@ 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.
+ /// \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 2b6e65efbf026..b4b7905f712db 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -846,17 +846,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{});
@@ -864,14 +867,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);
}
};
@@ -2015,7 +2019,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;
@@ -3683,6 +3688,32 @@ 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.
+ int Cmp = std::invoke(Compare, LHead, RHead);
+ if (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 9350ba7eb3de4..b0a6dc2be8c1b 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -272,6 +272,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);
}
@@ -1170,21 +1171,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..3813a5e481b91 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -1537,6 +1537,140 @@ 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"));
+ 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"));
+}
+
} // end namespace
} // end namespace format
} // end namespace clang
More information about the cfe-commits
mailing list