https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From 4bd306d48b35b1eb1d74186611cd7377d5387884 Mon Sep 17 00:00:00 2001 From: Shiakaron <[email protected]> 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 "a.h" #include "c.h" #include "bar/a.h" #include "bar/b.h" #include "foo/a.h" Assisted by: Github Copilot CLI - Claude Sonnet 4.6 --- clang/docs/ClangFormatStyleOptions.rst | 15 +++ clang/include/clang/Format/Format.h | 15 ++- clang/lib/Format/Format.cpp | 116 ++++++++++++++++---- clang/unittests/Format/ConfigParseTest.cpp | 1 + clang/unittests/Format/SortIncludesTest.cpp | 86 +++++++++++++++ 5 files changed, 210 insertions(+), 23 deletions(-) diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index 7b1b7a7384b07..f0ababe015138 100644 --- a/clang/docs/ClangFormatStyleOptions.rst +++ b/clang/docs/ClangFormatStyleOptions.rst @@ -6821,6 +6821,21 @@ 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. This applies equally + to ``""`` and ``<>`` includes. When ``false`` (default), the existing purely + alphabetical ordering is used. + + .. code-block:: c++ + + true: false (default): + #include "a.h" vs. #include "a.h" + #include "c.h" #include "bar/a.h" + #include "bar/a.h" #include "bar/b.h" + #include "bar/b.h" #include "c.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..87399f266373c 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 "c.h" #include "bar/a.h" + /// #include "bar/a.h" #include "bar/b.h" + /// #include "bar/b.h" #include "c.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/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index 205cc9bcb6d31..129bd4fb582d0 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); } diff --git a/clang/unittests/Format/SortIncludesTest.cpp b/clang/unittests/Format/SortIncludesTest.cpp index 48ecd5d32d034..5d548f82bba57 100644 --- a/clang/unittests/Format/SortIncludesTest.cpp +++ b/clang/unittests/Format/SortIncludesTest.cpp @@ -1503,6 +1503,92 @@ TEST_F(SortIncludesTest, IgnoreExtension) { "input.h")); } +TEST_F(SortIncludesTest, FilesBeforeFolders) { + // When false (default), sorting is purely alphabetical: "bar/" ('b') + // sorts before "c.h" ('c'), so subdirectories can appear between files. + FmtStyle.SortIncludes.FilesBeforeFolders = false; + verifyFormat("#include \"a.h\"\n" + "#include \"bar/a.h\"\n" + "#include \"bar/b.h\"\n" + "#include \"c.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 \"c.h\"", + "input.h")); + + // When true, all files at the current level sort before any subdirectory, + // so "c.h" moves ahead of "bar/" and "foo/". + FmtStyle.SortIncludes.FilesBeforeFolders = true; + verifyFormat("#include \"a.h\"\n" + "#include \"c.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 \"c.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
