[clang] [clang-format] Add FilesBeforeFolders option to SortIncludes (PR #208954)
Savvas Shiakas via cfe-commits
cfe-commits at lists.llvm.org
Sat Jul 11 14:16:42 PDT 2026
https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From 83a64805f70d03b5830d91062a9c08f75a2f3a64 Mon Sep 17 00:00:00 2001
From: Savvas Shiakas <savvas.shiakas at pharoscontrols.com>
Date: Sat, 11 Jul 2026 16:33:03 +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 "a.h"
#include "b.h"
#include "bar/a.h"
#include "bar/b.h"
#include "foo/a.h"
---
clang/docs/ClangFormatStyleOptions.rst | 13 +++
clang/include/clang/Format/Format.h | 15 ++-
clang/lib/Format/Format.cpp | 116 ++++++++++++++++----
clang/unittests/Format/SortIncludesTest.cpp | 71 ++++++++++++
4 files changed, 192 insertions(+), 23 deletions(-)
diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index 7b1b7a7384b07..4ac4d2168a6e7 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -6821,6 +6821,19 @@ the configuration (without a prefix: ``Auto``).
# include "A.inc" # include "A.h"
# include "A-util.h" # include "A.inc"
+ * ``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.
+
+ .. code-block:: c++
+
+ true: false:
+ #include "a.h" vs. #include "a.h"
+ #include "b.h" #include "bar/b.h"
+ #include "bar/a.h" #include "bar/a.h"
+ #include "bar/b.h" #include "b.h"
+ #include "foo/a.h" #include "foo/a.h"
+
.. _SortJavaStaticImport:
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 27b2d8f4a405b..a063e74e0373a 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -5074,9 +5074,22 @@ struct FormatStyle {
/// # include "A-util.h" # include "A.inc"
/// \endcode
bool IgnoreExtension;
+ /// 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.
+ /// \code
+ /// true: false:
+ /// #include "a.h" vs. #include "a.h"
+ /// #include "b.h" #include "bar/b.h"
+ /// #include "bar/a.h" #include "bar/a.h"
+ /// #include "bar/b.h" #include "b.h"
+ /// #include "foo/a.h" #include "foo/a.h"
+ /// \endcode
+ bool FilesBeforeFolders;
bool operator==(const SortIncludesOptions &R) const {
return Enabled == R.Enabled && IgnoreCase == R.IgnoreCase &&
- IgnoreExtension == R.IgnoreExtension;
+ IgnoreExtension == R.IgnoreExtension &&
+ 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 037111d8e9e5d..9e4c1712e3117 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -862,6 +862,7 @@ template <> struct MappingTraits<FormatStyle::SortIncludesOptions> {
IO.mapOptional("Enabled", Value.Enabled);
IO.mapOptional("IgnoreCase", Value.IgnoreCase);
IO.mapOptional("IgnoreExtension", Value.IgnoreExtension);
+ IO.mapOptional("FilesBeforeFolders", Value.FilesBeforeFolders);
}
};
@@ -1994,7 +1995,8 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
LLVMStyle.ShortNamespaceLines = 1;
LLVMStyle.SkipMacroDefinitionBody = false;
LLVMStyle.SortIncludes = {/*Enabled=*/true, /*IgnoreCase=*/false,
- /*IgnoreExtension=*/false};
+ /*IgnoreExtension=*/false,
+ /*FilesBeforeFolders=*/false};
LLVMStyle.SortJavaStaticImport = FormatStyle::SJSIO_Before;
LLVMStyle.SortUsingDeclarations = FormatStyle::SUD_LexicographicNumeric;
LLVMStyle.SpaceAfterCStyleCast = false;
@@ -3608,6 +3610,66 @@ std::string replaceCRLF(const std::string &Code) {
return NewCode;
}
+// Comparator that implements "files before folders" ordering at each directory
+// level, recursively. Files in a directory are listed alphabetically first,
+// then subdirectories (also alphabetically). This recurses into each
+// subdirectory.
+//
+// For example: "a.h", "b.h", "bar/a.h", "bar/b.h", "foo/a.h"
+// rather than: "a.h", "bar/a.h", "bar/b.h", "b.h", "foo/a.h"
+static bool filesBeforeFoldersLess(StringRef LHSFilename, StringRef RHSFilename,
+ bool IgnoreCase, bool IgnoreExtension) {
+ // Strip the leading and trailing delimiters (" or </>).
+ StringRef L = LHSFilename.substr(1, LHSFilename.size() - 2);
+ StringRef R = RHSFilename.substr(1, RHSFilename.size() - 2);
+
+ while (true) {
+ auto [LHead, LTail] = L.split('/');
+ auto [RHead, RTail] = R.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;
+
+ if (LIsFile) {
+ // Both are leaf filenames — compare them, optionally stripping
+ // extensions.
+ if (IgnoreExtension) {
+ StringRef LStem = LHead.rsplit('.').first;
+ StringRef RStem = RHead.rsplit('.').first;
+ // rsplit returns {original, ""} when no separator is found, so an
+ // extension-free name keeps its full value as the stem.
+ int StemCmp = IgnoreCase ? LStem.compare_insensitive(RStem)
+ : LStem.compare(RStem);
+ if (StemCmp != 0)
+ return StemCmp < 0;
+ // Stems are equal; use the full filename (including extension) as the
+ // tiebreaker, consistent with the non-FilesBeforeFolders
+ // IgnoreExtension behaviour.
+ int FullCmp = IgnoreCase ? LHead.compare_insensitive(RHead)
+ : LHead.compare(RHead);
+ return FullCmp < 0;
+ }
+ int Cmp =
+ IgnoreCase ? LHead.compare_insensitive(RHead) : LHead.compare(RHead);
+ return Cmp < 0;
+ }
+
+ // Both are directory components at this level — compare them.
+ int Cmp =
+ IgnoreCase ? LHead.compare_insensitive(RHead) : LHead.compare(RHead);
+ if (Cmp != 0)
+ return Cmp < 0;
+
+ // Components are equal; descend into the next path level.
+ L = LTail;
+ R = RTail;
+ }
+}
+
// Sorts and deduplicate a block of includes given by 'Includes' alphabetically
// adding the necessary replacement to 'Replaces'. 'Includes' must be in strict
// source order.
@@ -3631,27 +3693,37 @@ static void sortCppIncludes(const FormatStyle &Style,
llvm::to_vector<16>(llvm::seq<unsigned>(0, Includes.size()));
if (Style.SortIncludes.Enabled) {
- stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
- SmallString<128> LHSStem, RHSStem;
- if (Style.SortIncludes.IgnoreExtension) {
- LHSStem = Includes[LHSI].Filename;
- RHSStem = Includes[RHSI].Filename;
- llvm::sys::path::replace_extension(LHSStem, "");
- llvm::sys::path::replace_extension(RHSStem, "");
- }
- std::string LHSStemLower, RHSStemLower;
- std::string LHSFilenameLower, RHSFilenameLower;
- if (Style.SortIncludes.IgnoreCase) {
- LHSStemLower = LHSStem.str().lower();
- RHSStemLower = RHSStem.str().lower();
- LHSFilenameLower = Includes[LHSI].Filename.lower();
- RHSFilenameLower = Includes[RHSI].Filename.lower();
- }
- return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
- LHSFilenameLower, Includes[LHSI].Filename) <
- std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
- RHSFilenameLower, Includes[RHSI].Filename);
- });
+ if (Style.SortIncludes.FilesBeforeFolders) {
+ stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
+ if (Includes[LHSI].Priority != Includes[RHSI].Priority)
+ return Includes[LHSI].Priority < Includes[RHSI].Priority;
+ return filesBeforeFoldersLess(
+ Includes[LHSI].Filename, Includes[RHSI].Filename,
+ Style.SortIncludes.IgnoreCase, Style.SortIncludes.IgnoreExtension);
+ });
+ } else {
+ stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
+ SmallString<128> LHSStem, RHSStem;
+ if (Style.SortIncludes.IgnoreExtension) {
+ LHSStem = Includes[LHSI].Filename;
+ RHSStem = Includes[RHSI].Filename;
+ llvm::sys::path::replace_extension(LHSStem, "");
+ llvm::sys::path::replace_extension(RHSStem, "");
+ }
+ std::string LHSStemLower, RHSStemLower;
+ std::string LHSFilenameLower, RHSFilenameLower;
+ if (Style.SortIncludes.IgnoreCase) {
+ LHSStemLower = LHSStem.str().lower();
+ RHSStemLower = RHSStem.str().lower();
+ LHSFilenameLower = Includes[LHSI].Filename.lower();
+ RHSFilenameLower = Includes[RHSI].Filename.lower();
+ }
+ return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
+ LHSFilenameLower, Includes[LHSI].Filename) <
+ std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
+ RHSFilenameLower, Includes[RHSI].Filename);
+ });
+ }
}
// The index of the include on which the cursor will be put after
diff --git a/clang/unittests/Format/SortIncludesTest.cpp b/clang/unittests/Format/SortIncludesTest.cpp
index 48ecd5d32d034..a8b185761ef80 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -1503,6 +1503,77 @@ TEST_F(SortIncludesTest, IgnoreExtension) {
"input.h"));
}
+TEST_F(SortIncludesTest, FilesBeforeFolders) {
+ FmtStyle.SortIncludes.FilesBeforeFolders = true;
+
+ // Files at the same directory level sort before subdirectories.
+ verifyFormat("#include \"a.h\"\n"
+ "#include \"b.h\"\n"
+ "#include \"bar/a.h\"\n"
+ "#include \"bar/b.h\"\n"
+ "#include \"foo/a.h\"",
+ sort("#include \"bar/b.h\"\n"
+ "#include \"a.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"bar/a.h\"\n"
+ "#include \"b.h\"",
+ "input.h"));
+
+ // Files and folders are each sorted alphabetically at each level.
+ verifyFormat("#include \"a.h\"\n"
+ "#include \"b.h\"\n"
+ "#include \"c.h\"\n"
+ "#include \"bar/x.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"foo/b.h\"",
+ sort("#include \"foo/b.h\"\n"
+ "#include \"bar/x.h\"\n"
+ "#include \"c.h\"\n"
+ "#include \"a.h\"\n"
+ "#include \"foo/a.h\"\n"
+ "#include \"b.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;
+}
+
} // end namespace
} // end namespace format
} // end namespace clang
More information about the cfe-commits
mailing list