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 <[email protected]> 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 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
