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

Reply via email to