https://github.com/Shiakaron updated https://github.com/llvm/llvm-project/pull/208954
>From be33e64dabc26c6c9f2f67aad8180fad3338cffa 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.md | 22 +++ clang/include/clang/Format/Format.h | 25 +++- clang/lib/Format/Format.cpp | 48 +++++- clang/unittests/Format/ConfigParseTest.cpp | 13 +- clang/unittests/Format/SortIncludesTest.cpp | 158 ++++++++++++++++++++ 5 files changed, 255 insertions(+), 11 deletions(-) diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md index ee239f768cf2b..0f4fcb6a42a44 100644 --- a/clang/docs/ClangFormatStyleOptions.md +++ b/clang/docs/ClangFormatStyleOptions.md @@ -6989,6 +6989,28 @@ the configuration (without a prefix: `Auto`). #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. + + This option is a secondary sort key within each ``Priority`` group + defined by ``IncludeCategories``. Includes in different ``Priority`` + groups are still separated by that primary ordering. + + ```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 0f91df5b1b32d..ddef3659af65b 100644 --- a/clang/include/clang/Format/Format.h +++ b/clang/include/clang/Format/Format.h @@ -5106,9 +5106,32 @@ 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. + /// + /// This option is a secondary sort key within each ``Priority`` group + /// defined by ``IncludeCategories``. Includes in different ``Priority`` + /// groups are still separated by that primary ordering. + /// \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 574014f360354..d1798df7333c5 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -849,17 +849,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{}); @@ -867,14 +870,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); } }; @@ -2023,7 +2027,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; @@ -3691,6 +3696,37 @@ 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. + if (Style.SortIncludes.IgnoreCase) { + int Cmp = std::invoke(Compare, StringRef(LHead.lower()), + StringRef(RHead.lower())); + if (Cmp != 0) + return Cmp < 0; + } + if (int Cmp = std::invoke(Compare, LHead, RHead); 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 53fbec9806ca4..2076e4dacfcb0 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -273,6 +273,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); } @@ -1171,21 +1172,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..fe193b7a43df2 100644 --- a/clang/unittests/Format/SortIncludesTest.cpp +++ b/clang/unittests/Format/SortIncludesTest.cpp @@ -1537,6 +1537,164 @@ 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")); + // Case-sensitive comparison is the tiebreaker when directory components are + // case-insensitively equal: "Bar" (0x42) < "bar" (0x62). + verifyFormat("#include \"Bar/a.h\"\n" + "#include \"bar/a.h\"", + sort("#include \"bar/a.h\"\n" + "#include \"Bar/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")); +} + +TEST_F(SortIncludesTest, FilesBeforeFoldersWithPriority) { + // FilesBeforeFolders is a secondary sort key: Priority groups stay separate + // and FilesBeforeFolders only reorders includes within the same group. + Style.IncludeBlocks = tooling::IncludeStyle::IBS_Regroup; + Style.IncludeCategories = {{"^<", 1, 0, false}, {"^\"", 2, 0, false}}; + FmtStyle.SortIncludes.FilesBeforeFolders = true; + verifyFormat("#include <stdio.h>\n" + "#include <sys/stat.h>\n" + "\n" + "#include \"utils.h\"\n" + "#include \"foo/bar.h\"", + sort("#include <sys/stat.h>\n" + "#include \"foo/bar.h\"\n" + "#include <stdio.h>\n" + "#include \"utils.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
