https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From 0b88b235798b4a9ebed8aa18bcc84d8248cc014d 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 "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 _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
