Author: Savvas Shiakas Date: 2026-09-04T20:53:06Z New Revision: 6bfbb2ef1bf54cf1b0491226094a2a7bd5c6d6c5
URL: https://github.com/llvm/llvm-project/commit/6bfbb2ef1bf54cf1b0491226094a2a7bd5c6d6c5 DIFF: https://github.com/llvm/llvm-project/commit/6bfbb2ef1bf54cf1b0491226094a2a7bd5c6d6c5.diff LOG: [clang-format] Add FilesBeforeFolders option to SortIncludes (#208954) 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: ``` 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" ``` Assisted-by: Github Copilot CLI Added: Modified: clang/docs/ClangFormatStyleOptions.md clang/include/clang/Format/Format.h clang/lib/Format/Format.cpp clang/unittests/Format/ConfigParseTest.cpp clang/unittests/Format/SortIncludesTest.cpp Removed: ################################################################################ diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md index f575f61745cd6..81984ff185e53 100644 --- a/clang/docs/ClangFormatStyleOptions.md +++ b/clang/docs/ClangFormatStyleOptions.md @@ -7006,6 +7006,29 @@ 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 diff erent `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 6deb5bcbea80c..baf56a9937957 100644 --- a/clang/include/clang/Format/Format.h +++ b/clang/include/clang/Format/Format.h @@ -5122,9 +5122,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 diff erent `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 eec0a9dbced81..4c78c1dbe9f80 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -850,17 +850,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{}); @@ -868,14 +871,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); } }; @@ -2029,7 +2033,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; @@ -3697,6 +3702,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 6d12dfd715ae3..86511edb9d40d 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -274,6 +274,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); } @@ -1172,21 +1173,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..ac2b227a9e255 100644 --- a/clang/unittests/Format/SortIncludesTest.cpp +++ b/clang/unittests/Format/SortIncludesTest.cpp @@ -1537,6 +1537,124 @@ TEST_F(SortIncludesTest, IgnoreExtension) { "input.h")); } +TEST_F(SortIncludesTest, FilesBeforeFolders) { + 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")); + + 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")); + + 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")); + + 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")); + 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; + + 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; + + 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) { + 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
