llvmorg-github-actions[bot] wrote:

<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-format

Author: Savvas Shiakas (Shiakaron)

<details>
<summary>Changes</summary>

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 "a.h"
  #include "b.h"
  #include "bar/a.h"
  #include "bar/b.h"
  #include "foo/a.h"

---
Full diff: https://github.com/llvm/llvm-project/pull/208954.diff


4 Files Affected:

- (modified) clang/docs/ClangFormatStyleOptions.rst (+13) 
- (modified) clang/include/clang/Format/Format.h (+14-1) 
- (modified) clang/lib/Format/Format.cpp (+94-22) 
- (modified) clang/unittests/Format/SortIncludesTest.cpp (+71) 


``````````diff
diff --git a/clang/docs/ClangFormatStyleOptions.rst 
b/clang/docs/ClangFormatStyleOptions.rst
index 7b1b7a7384b07..4ac4d2168a6e7 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -6821,6 +6821,19 @@ the configuration (without a prefix: ``Auto``).
        # include "A.inc"              # include "A.h"
        # include "A-util.h"           # include "A.inc"
 
+  * ``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.
+
+    .. code-block:: c++
+
+       true:                          false:
+       #include "a.h"         vs.    #include "a.h"
+       #include "b.h"                #include "bar/b.h"
+       #include "bar/a.h"            #include "bar/a.h"
+       #include "bar/b.h"            #include "b.h"
+       #include "foo/a.h"            #include "foo/a.h"
+
 
 .. _SortJavaStaticImport:
 
diff --git a/clang/include/clang/Format/Format.h 
b/clang/include/clang/Format/Format.h
index 27b2d8f4a405b..a063e74e0373a 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -5074,9 +5074,22 @@ struct FormatStyle {
     ///    # include "A-util.h"           # include "A.inc"
     /// \endcode
     bool IgnoreExtension;
+    /// 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.
+    /// \code
+    ///    true:                          false:
+    ///    #include "a.h"         vs.    #include "a.h"
+    ///    #include "b.h"                #include "bar/b.h"
+    ///    #include "bar/a.h"            #include "bar/a.h"
+    ///    #include "bar/b.h"            #include "b.h"
+    ///    #include "foo/a.h"            #include "foo/a.h"
+    /// \endcode
+    bool FilesBeforeFolders;
     bool operator==(const SortIncludesOptions &R) const {
       return Enabled == R.Enabled && IgnoreCase == R.IgnoreCase &&
-             IgnoreExtension == R.IgnoreExtension;
+             IgnoreExtension == R.IgnoreExtension &&
+             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 037111d8e9e5d..9e4c1712e3117 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -862,6 +862,7 @@ template <> struct 
MappingTraits<FormatStyle::SortIncludesOptions> {
     IO.mapOptional("Enabled", Value.Enabled);
     IO.mapOptional("IgnoreCase", Value.IgnoreCase);
     IO.mapOptional("IgnoreExtension", Value.IgnoreExtension);
+    IO.mapOptional("FilesBeforeFolders", Value.FilesBeforeFolders);
   }
 };
 
@@ -1994,7 +1995,8 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind 
Language) {
   LLVMStyle.ShortNamespaceLines = 1;
   LLVMStyle.SkipMacroDefinitionBody = false;
   LLVMStyle.SortIncludes = {/*Enabled=*/true, /*IgnoreCase=*/false,
-                            /*IgnoreExtension=*/false};
+                            /*IgnoreExtension=*/false,
+                            /*FilesBeforeFolders=*/false};
   LLVMStyle.SortJavaStaticImport = FormatStyle::SJSIO_Before;
   LLVMStyle.SortUsingDeclarations = FormatStyle::SUD_LexicographicNumeric;
   LLVMStyle.SpaceAfterCStyleCast = false;
@@ -3608,6 +3610,66 @@ std::string replaceCRLF(const std::string &Code) {
   return NewCode;
 }
 
+// Comparator that implements "files before folders" ordering at each directory
+// level, recursively. Files in a directory are listed alphabetically first,
+// then subdirectories (also alphabetically). This recurses into each
+// subdirectory.
+//
+// For example: "a.h", "b.h", "bar/a.h", "bar/b.h", "foo/a.h"
+// rather than: "a.h", "bar/a.h", "bar/b.h", "b.h", "foo/a.h"
+static bool filesBeforeFoldersLess(StringRef LHSFilename, StringRef 
RHSFilename,
+                                   bool IgnoreCase, bool IgnoreExtension) {
+  // Strip the leading and trailing delimiters (" or </>).
+  StringRef L = LHSFilename.substr(1, LHSFilename.size() - 2);
+  StringRef R = RHSFilename.substr(1, RHSFilename.size() - 2);
+
+  while (true) {
+    auto [LHead, LTail] = L.split('/');
+    auto [RHead, RTail] = R.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;
+
+    if (LIsFile) {
+      // Both are leaf filenames — compare them, optionally stripping
+      // extensions.
+      if (IgnoreExtension) {
+        StringRef LStem = LHead.rsplit('.').first;
+        StringRef RStem = RHead.rsplit('.').first;
+        // rsplit returns {original, ""} when no separator is found, so an
+        // extension-free name keeps its full value as the stem.
+        int StemCmp = IgnoreCase ? LStem.compare_insensitive(RStem)
+                                 : LStem.compare(RStem);
+        if (StemCmp != 0)
+          return StemCmp < 0;
+        // Stems are equal; use the full filename (including extension) as the
+        // tiebreaker, consistent with the non-FilesBeforeFolders
+        // IgnoreExtension behaviour.
+        int FullCmp = IgnoreCase ? LHead.compare_insensitive(RHead)
+                                 : LHead.compare(RHead);
+        return FullCmp < 0;
+      }
+      int Cmp =
+          IgnoreCase ? LHead.compare_insensitive(RHead) : LHead.compare(RHead);
+      return Cmp < 0;
+    }
+
+    // Both are directory components at this level — compare them.
+    int Cmp =
+        IgnoreCase ? LHead.compare_insensitive(RHead) : LHead.compare(RHead);
+    if (Cmp != 0)
+      return Cmp < 0;
+
+    // Components are equal; descend into the next path level.
+    L = LTail;
+    R = RTail;
+  }
+}
+
 // Sorts and deduplicate a block of includes given by 'Includes' alphabetically
 // adding the necessary replacement to 'Replaces'. 'Includes' must be in strict
 // source order.
@@ -3631,27 +3693,37 @@ static void sortCppIncludes(const FormatStyle &Style,
       llvm::to_vector<16>(llvm::seq<unsigned>(0, Includes.size()));
 
   if (Style.SortIncludes.Enabled) {
-    stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
-      SmallString<128> LHSStem, RHSStem;
-      if (Style.SortIncludes.IgnoreExtension) {
-        LHSStem = Includes[LHSI].Filename;
-        RHSStem = Includes[RHSI].Filename;
-        llvm::sys::path::replace_extension(LHSStem, "");
-        llvm::sys::path::replace_extension(RHSStem, "");
-      }
-      std::string LHSStemLower, RHSStemLower;
-      std::string LHSFilenameLower, RHSFilenameLower;
-      if (Style.SortIncludes.IgnoreCase) {
-        LHSStemLower = LHSStem.str().lower();
-        RHSStemLower = RHSStem.str().lower();
-        LHSFilenameLower = Includes[LHSI].Filename.lower();
-        RHSFilenameLower = Includes[RHSI].Filename.lower();
-      }
-      return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
-                      LHSFilenameLower, Includes[LHSI].Filename) <
-             std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
-                      RHSFilenameLower, Includes[RHSI].Filename);
-    });
+    if (Style.SortIncludes.FilesBeforeFolders) {
+      stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
+        if (Includes[LHSI].Priority != Includes[RHSI].Priority)
+          return Includes[LHSI].Priority < Includes[RHSI].Priority;
+        return filesBeforeFoldersLess(
+            Includes[LHSI].Filename, Includes[RHSI].Filename,
+            Style.SortIncludes.IgnoreCase, Style.SortIncludes.IgnoreExtension);
+      });
+    } else {
+      stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
+        SmallString<128> LHSStem, RHSStem;
+        if (Style.SortIncludes.IgnoreExtension) {
+          LHSStem = Includes[LHSI].Filename;
+          RHSStem = Includes[RHSI].Filename;
+          llvm::sys::path::replace_extension(LHSStem, "");
+          llvm::sys::path::replace_extension(RHSStem, "");
+        }
+        std::string LHSStemLower, RHSStemLower;
+        std::string LHSFilenameLower, RHSFilenameLower;
+        if (Style.SortIncludes.IgnoreCase) {
+          LHSStemLower = LHSStem.str().lower();
+          RHSStemLower = RHSStem.str().lower();
+          LHSFilenameLower = Includes[LHSI].Filename.lower();
+          RHSFilenameLower = Includes[RHSI].Filename.lower();
+        }
+        return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
+                        LHSFilenameLower, Includes[LHSI].Filename) <
+               std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
+                        RHSFilenameLower, Includes[RHSI].Filename);
+      });
+    }
   }
 
   // The index of the include on which the cursor will be put after
diff --git a/clang/unittests/Format/SortIncludesTest.cpp 
b/clang/unittests/Format/SortIncludesTest.cpp
index 48ecd5d32d034..a8b185761ef80 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -1503,6 +1503,77 @@ TEST_F(SortIncludesTest, IgnoreExtension) {
                     "input.h"));
 }
 
+TEST_F(SortIncludesTest, FilesBeforeFolders) {
+  FmtStyle.SortIncludes.FilesBeforeFolders = true;
+
+  // Files at the same directory level sort before subdirectories.
+  verifyFormat("#include \"a.h\"\n"
+               "#include \"b.h\"\n"
+               "#include \"bar/a.h\"\n"
+               "#include \"bar/b.h\"\n"
+               "#include \"foo/a.h\"",
+               sort("#include \"bar/b.h\"\n"
+                    "#include \"a.h\"\n"
+                    "#include \"foo/a.h\"\n"
+                    "#include \"bar/a.h\"\n"
+                    "#include \"b.h\"",
+                    "input.h"));
+
+  // Files and folders are each sorted alphabetically at each level.
+  verifyFormat("#include \"a.h\"\n"
+               "#include \"b.h\"\n"
+               "#include \"c.h\"\n"
+               "#include \"bar/x.h\"\n"
+               "#include \"foo/a.h\"\n"
+               "#include \"foo/b.h\"",
+               sort("#include \"foo/b.h\"\n"
+                    "#include \"bar/x.h\"\n"
+                    "#include \"c.h\"\n"
+                    "#include \"a.h\"\n"
+                    "#include \"foo/a.h\"\n"
+                    "#include \"b.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;
+}
+
 } // end namespace
 } // end namespace format
 } // end namespace clang

``````````

</details>


https://github.com/llvm/llvm-project/pull/208954
_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to