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

Reply via email to