[clang] [clang-format] Add Natural option for SortIncludes (PR #210788)

Tshaka Lekholoane via cfe-commits cfe-commits at lists.llvm.org
Mon Jul 27 13:49:34 PDT 2026


https://github.com/tshakalekholoane updated https://github.com/llvm/llvm-project/pull/210788

>From 0d576f2d6e93abac36ac22377786516c81139f7a Mon Sep 17 00:00:00 2001
From: Tshaka Lekholoane <mail+git at tshaka.dev>
Date: Mon, 20 Jul 2026 16:08:43 +0200
Subject: [PATCH 1/3] [clang-format] Add Natural option for SortIncludes

SortIncludes currently orders includes lexicographically with the option
to ignore case or extension. This adds another option, Natural, that
compares embedded runs of digits as numbers rather than sequences of
characters, matching the "natural sort" behaviour found in most file
managers and tools like sort when called with the -V option.
---
 clang/docs/ClangFormatStyleOptions.rst      | 10 ++++++
 clang/include/clang/Format/Format.h         | 11 ++++++-
 clang/lib/Format/Format.cpp                 | 35 ++++++++++++++++++---
 clang/unittests/Format/ConfigParseTest.cpp  | 28 ++++++++++-------
 clang/unittests/Format/SortIncludesTest.cpp | 34 ++++++++++++++++++++
 5 files changed, 101 insertions(+), 17 deletions(-)

diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index 7b1b7a7384b07..7c5f42b974fb7 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -6821,6 +6821,16 @@ the configuration (without a prefix: ``Auto``).
        # include "A.inc"              # include "A.h"
        # include "A-util.h"           # include "A.inc"
 
+  * ``bool Natural`` Whether or not includes are sorted by natural ordering i.e., whether
+    embedded runs of digits are compared as numbers rather than sequences of
+    characters.
+
+    .. code-block:: c++
+
+       true:                      false:
+       #include "A2.h"     vs.    #include "A10.h"
+       #include "A10.h"           #include "A2.h"
+
 
 .. _SortJavaStaticImport:
 
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 27b2d8f4a405b..0c8acd6c4dbbb 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -5074,9 +5074,18 @@ struct FormatStyle {
     ///    # include "A-util.h"           # include "A.inc"
     /// \endcode
     bool IgnoreExtension;
+    /// Whether or not includes are sorted by natural ordering i.e., whether
+    /// embedded runs of digits are compared as numbers rather than sequences of
+    /// characters.
+    /// \code
+    ///    true:                      false:
+    ///    #include "A2.h"     vs.    #include "A10.h"
+    ///    #include "A10.h"           #include "A2.h"
+    /// \endcode
+    bool Natural;
     bool operator==(const SortIncludesOptions &R) const {
       return Enabled == R.Enabled && IgnoreCase == R.IgnoreCase &&
-             IgnoreExtension == R.IgnoreExtension;
+             IgnoreExtension == R.IgnoreExtension && Natural == R.Natural;
     }
     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..8d07a17a3a7e9 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -844,24 +844,33 @@ template <> struct MappingTraits<FormatStyle::SortIncludesOptions> {
     IO.enumCase(Value, "CaseInsensitive",
                 FormatStyle::SortIncludesOptions{/*Enabled=*/true,
                                                  /*IgnoreCase=*/true,
-                                                 /*IgnoreExtension=*/false});
+                                                 /*IgnoreExtension=*/false,
+                                                 /*Natural=*/false});
     IO.enumCase(Value, "CaseSensitive",
                 FormatStyle::SortIncludesOptions{/*Enabled=*/true,
                                                  /*IgnoreCase=*/false,
-                                                 /*IgnoreExtension=*/false});
+                                                 /*IgnoreExtension=*/false,
+                                                 /*Natural=*/false});
+    IO.enumCase(Value, "Natural",
+                FormatStyle::SortIncludesOptions{/*Enabled=*/true,
+                                                 /*IgnoreCase=*/false,
+                                                 /*IgnoreExtension=*/false,
+                                                 /*Natural=*/true});
 
     // For backward compatibility.
     IO.enumCase(Value, "false", FormatStyle::SortIncludesOptions{});
     IO.enumCase(Value, "true",
                 FormatStyle::SortIncludesOptions{/*Enabled=*/true,
                                                  /*IgnoreCase=*/false,
-                                                 /*IgnoreExtension=*/false});
+                                                 /*IgnoreExtension=*/false,
+                                                 /*Natural=*/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);
   }
 };
 
@@ -1994,7 +2003,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.ShortNamespaceLines = 1;
   LLVMStyle.SkipMacroDefinitionBody = false;
   LLVMStyle.SortIncludes = {/*Enabled=*/true, /*IgnoreCase=*/false,
-                            /*IgnoreExtension=*/false};
+                            /*IgnoreExtension=*/false, /*Natural=*/false};
   LLVMStyle.SortJavaStaticImport = FormatStyle::SJSIO_Before;
   LLVMStyle.SortUsingDeclarations = FormatStyle::SUD_LexicographicNumeric;
   LLVMStyle.SpaceAfterCStyleCast = false;
@@ -3647,6 +3656,24 @@ static void sortCppIncludes(const FormatStyle &Style,
         LHSFilenameLower = Includes[LHSI].Filename.lower();
         RHSFilenameLower = Includes[RHSI].Filename.lower();
       }
+      if (Style.SortIncludes.Natural) {
+        if (Includes[LHSI].Priority != Includes[RHSI].Priority)
+          return Includes[LHSI].Priority < Includes[RHSI].Priority;
+        if (Style.SortIncludes.IgnoreCase) {
+          if (int Cmp = StringRef(LHSStemLower).compare_numeric(RHSStemLower))
+            return Cmp < 0;
+        }
+        if (int Cmp = StringRef(LHSStem).compare_numeric(RHSStem))
+          return Cmp < 0;
+        if (Style.SortIncludes.IgnoreCase) {
+          if (int Cmp = StringRef(LHSFilenameLower)
+                            .compare_numeric(RHSFilenameLower)) {
+            return Cmp < 0;
+          }
+        }
+        return StringRef(Includes[LHSI].Filename)
+                   .compare_numeric(Includes[RHSI].Filename) < 0;
+      }
       return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
                       LHSFilenameLower, Includes[LHSI].Filename) <
              std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 205cc9bcb6d31..51e59345325a9 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -1167,20 +1167,24 @@ TEST(ConfigParseTest, ParsesConfiguration) {
               IncludeStyle.IncludeIsMainSourceRegex, "abc$");
 
   Style.SortIncludes = {};
-  CHECK_PARSE(
-      "SortIncludes: true", SortIncludes,
-      FormatStyle::SortIncludesOptions(
-          {/*Enabled=*/true, /*IgnoreCase=*/false, /*IgnoreExtension=*/false}));
+  CHECK_PARSE("SortIncludes: true", SortIncludes,
+              FormatStyle::SortIncludesOptions(
+                  {/*Enabled=*/true, /*IgnoreCase=*/false,
+                   /*IgnoreExtension=*/false, /*Natural=*/false}));
   CHECK_PARSE("SortIncludes: false", SortIncludes,
               FormatStyle::SortIncludesOptions{});
-  CHECK_PARSE(
-      "SortIncludes: CaseInsensitive", SortIncludes,
-      FormatStyle::SortIncludesOptions(
-          {/*Enabled=*/true, /*IgnoreCase=*/true, /*IgnoreExtension=*/false}));
-  CHECK_PARSE(
-      "SortIncludes: CaseSensitive", SortIncludes,
-      FormatStyle::SortIncludesOptions(
-          {/*Enabled=*/true, /*IgnoreCase=*/false, /*IgnoreExtension=*/false}));
+  CHECK_PARSE("SortIncludes: CaseInsensitive", SortIncludes,
+              FormatStyle::SortIncludesOptions(
+                  {/*Enabled=*/true, /*IgnoreCase=*/true,
+                   /*IgnoreExtension=*/false, /*Natural=*/false}));
+  CHECK_PARSE("SortIncludes: CaseSensitive", SortIncludes,
+              FormatStyle::SortIncludesOptions(
+                  {/*Enabled=*/true, /*IgnoreCase=*/false,
+                   /*IgnoreExtension=*/false, /*Natural=*/false}));
+  CHECK_PARSE("SortIncludes: Natural", SortIncludes,
+              FormatStyle::SortIncludesOptions(
+                  {/*Enabled=*/true, /*IgnoreCase=*/false,
+                   /*IgnoreExtension=*/false, /*Natural=*/true}));
   CHECK_PARSE("SortIncludes: Never", SortIncludes,
               FormatStyle::SortIncludesOptions{});
 
diff --git a/clang/unittests/Format/SortIncludesTest.cpp b/clang/unittests/Format/SortIncludesTest.cpp
index 48ecd5d32d034..a6e9e18496f8d 100644
--- a/clang/unittests/Format/SortIncludesTest.cpp
+++ b/clang/unittests/Format/SortIncludesTest.cpp
@@ -668,6 +668,40 @@ TEST_F(SortIncludesTest, SupportOptionalCaseSensitiveSorting) {
                sort(UnsortedCode));
 }
 
+TEST_F(SortIncludesTest, SupportNaturalSorting) {
+  FmtStyle.SortIncludes.Natural = true;
+  verifyFormat("#include \"crypto/chacha8.h\"\n"
+               "#include \"crypto/chacha12.h\"\n"
+               "#include \"crypto/chacha20.h\"",
+               sort("#include \"crypto/chacha12.h\"\n"
+                    "#include \"crypto/chacha8.h\"\n"
+                    "#include \"crypto/chacha20.h\""));
+}
+
+TEST_F(SortIncludesTest, SupportNaturalSortingWithIgnoreCase) {
+  FmtStyle.SortIncludes.Natural = true;
+  FmtStyle.SortIncludes.IgnoreCase = true;
+
+  verifyFormat("#include \"crypto/chacha8.h\"\n"
+               "#include \"Crypto/ChaCha12.h\"\n"
+               "#include \"crypto/chacha20.h\"",
+               sort("#include \"Crypto/ChaCha12.h\"\n"
+                    "#include \"crypto/chacha8.h\"\n"
+                    "#include \"crypto/chacha20.h\""));
+}
+
+TEST_F(SortIncludesTest, SupportNaturalSortingWithIgnoreExtension) {
+  FmtStyle.SortIncludes.Natural = true;
+  FmtStyle.SortIncludes.IgnoreExtension = true;
+
+  verifyFormat("#include \"crypto/chacha8.c\"\n"
+               "#include \"crypto/chacha12.h\"\n"
+               "#include \"crypto/chacha20.h\"",
+               sort("#include \"crypto/chacha12.h\"\n"
+                    "#include \"crypto/chacha20.h\"\n"
+                    "#include \"crypto/chacha8.c\""));
+}
+
 TEST_F(SortIncludesTest, SupportCaseInsensitiveMatching) {
   // Setup an regex for main includes so we can cover those as well.
   Style.IncludeIsMainRegex = "([-_](test|unittest))?$";

>From d532ddbd1a8e6a9a9171c1b2c0ab11521b276043 Mon Sep 17 00:00:00 2001
From: Tshaka Lekholoane <mail+git at tshaka.dev>
Date: Sat, 25 Jul 2026 12:07:35 +0200
Subject: [PATCH 2/3] [clang-format] Remove duplicate logic in SortIncludes
 sort comparisons

---
 clang/lib/Format/Format.cpp | 41 +++++++++++++++++++------------------
 1 file changed, 21 insertions(+), 20 deletions(-)

diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 8d07a17a3a7e9..f10481dcc94fd 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -3641,6 +3641,9 @@ static void sortCppIncludes(const FormatStyle &Style,
 
   if (Style.SortIncludes.Enabled) {
     stable_sort(Indices, [&](unsigned LHSI, unsigned RHSI) {
+      if (Includes[LHSI].Priority != Includes[RHSI].Priority)
+        return Includes[LHSI].Priority < Includes[RHSI].Priority;
+
       SmallString<128> LHSStem, RHSStem;
       if (Style.SortIncludes.IgnoreExtension) {
         LHSStem = Includes[LHSI].Filename;
@@ -3648,6 +3651,7 @@ static void sortCppIncludes(const FormatStyle &Style,
         llvm::sys::path::replace_extension(LHSStem, "");
         llvm::sys::path::replace_extension(RHSStem, "");
       }
+
       std::string LHSStemLower, RHSStemLower;
       std::string LHSFilenameLower, RHSFilenameLower;
       if (Style.SortIncludes.IgnoreCase) {
@@ -3656,28 +3660,25 @@ static void sortCppIncludes(const FormatStyle &Style,
         LHSFilenameLower = Includes[LHSI].Filename.lower();
         RHSFilenameLower = Includes[RHSI].Filename.lower();
       }
-      if (Style.SortIncludes.Natural) {
-        if (Includes[LHSI].Priority != Includes[RHSI].Priority)
-          return Includes[LHSI].Priority < Includes[RHSI].Priority;
-        if (Style.SortIncludes.IgnoreCase) {
-          if (int Cmp = StringRef(LHSStemLower).compare_numeric(RHSStemLower))
-            return Cmp < 0;
-        }
-        if (int Cmp = StringRef(LHSStem).compare_numeric(RHSStem))
+
+      const auto Compare = Style.SortIncludes.Natural
+                               ? &StringRef::compare_numeric
+                               : &StringRef::compare;
+
+      if (Style.SortIncludes.IgnoreCase) {
+        if (int Cmp = (StringRef(LHSStemLower).*Compare)(RHSStemLower))
           return Cmp < 0;
-        if (Style.SortIncludes.IgnoreCase) {
-          if (int Cmp = StringRef(LHSFilenameLower)
-                            .compare_numeric(RHSFilenameLower)) {
-            return Cmp < 0;
-          }
-        }
-        return StringRef(Includes[LHSI].Filename)
-                   .compare_numeric(Includes[RHSI].Filename) < 0;
       }
-      return std::tie(Includes[LHSI].Priority, LHSStemLower, LHSStem,
-                      LHSFilenameLower, Includes[LHSI].Filename) <
-             std::tie(Includes[RHSI].Priority, RHSStemLower, RHSStem,
-                      RHSFilenameLower, Includes[RHSI].Filename);
+
+      if (int Cmp = (StringRef(LHSStem).*Compare)(RHSStem))
+        return Cmp < 0;
+
+      if (Style.SortIncludes.IgnoreCase) {
+        if (int Cmp = (StringRef(LHSFilenameLower).*Compare)(RHSFilenameLower))
+          return Cmp < 0;
+      }
+
+      return (Includes[LHSI].Filename.*Compare)(Includes[RHSI].Filename) < 0;
     });
   }
 

>From 0ff50ee9b5807cd1bc394dfa08f4c4454a151f84 Mon Sep 17 00:00:00 2001
From: Tshaka Lekholoane <mail+git at tshaka.dev>
Date: Mon, 27 Jul 2026 13:40:33 +0200
Subject: [PATCH 3/3] [clang-format] Avoid comparing empty stems and use invoke

---
 clang/lib/Format/Format.cpp | 33 +++++++++++++++++++++------------
 1 file changed, 21 insertions(+), 12 deletions(-)

diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index f10481dcc94fd..7d55c738a9ca3 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -25,6 +25,7 @@
 #include "clang/Tooling/Inclusions/HeaderIncludes.h"
 #include "llvm/ADT/Sequence.h"
 #include "llvm/ADT/StringSet.h"
+#include <functional>
 #include <limits>
 
 #define DEBUG_TYPE "format-formatter"
@@ -3644,19 +3645,24 @@ static void sortCppIncludes(const FormatStyle &Style,
       if (Includes[LHSI].Priority != Includes[RHSI].Priority)
         return Includes[LHSI].Priority < Includes[RHSI].Priority;
 
-      SmallString<128> LHSStem, RHSStem;
+      auto LHSStem = Includes[LHSI].Filename;
+      auto RHSStem = Includes[RHSI].Filename;
+
+      SmallString<128> LHSStemStorage, RHSStemStorage;
       if (Style.SortIncludes.IgnoreExtension) {
-        LHSStem = Includes[LHSI].Filename;
-        RHSStem = Includes[RHSI].Filename;
-        llvm::sys::path::replace_extension(LHSStem, "");
-        llvm::sys::path::replace_extension(RHSStem, "");
+        LHSStemStorage = Includes[LHSI].Filename;
+        RHSStemStorage = Includes[RHSI].Filename;
+        llvm::sys::path::replace_extension(LHSStemStorage, "");
+        llvm::sys::path::replace_extension(RHSStemStorage, "");
+        LHSStem = LHSStemStorage;
+        RHSStem = RHSStemStorage;
       }
 
       std::string LHSStemLower, RHSStemLower;
       std::string LHSFilenameLower, RHSFilenameLower;
       if (Style.SortIncludes.IgnoreCase) {
-        LHSStemLower = LHSStem.str().lower();
-        RHSStemLower = RHSStem.str().lower();
+        LHSStemLower = LHSStem.lower();
+        RHSStemLower = RHSStem.lower();
         LHSFilenameLower = Includes[LHSI].Filename.lower();
         RHSFilenameLower = Includes[RHSI].Filename.lower();
       }
@@ -3666,19 +3672,22 @@ static void sortCppIncludes(const FormatStyle &Style,
                                : &StringRef::compare;
 
       if (Style.SortIncludes.IgnoreCase) {
-        if (int Cmp = (StringRef(LHSStemLower).*Compare)(RHSStemLower))
+        int Cmp = std::invoke(Compare, StringRef(LHSStemLower), RHSStemLower);
+        if (Cmp != 0)
           return Cmp < 0;
       }
 
-      if (int Cmp = (StringRef(LHSStem).*Compare)(RHSStem))
+      if (int Cmp = std::invoke(Compare, LHSStem, RHSStem); Cmp != 0)
         return Cmp < 0;
 
       if (Style.SortIncludes.IgnoreCase) {
-        if (int Cmp = (StringRef(LHSFilenameLower).*Compare)(RHSFilenameLower))
+        int Cmp =
+            std::invoke(Compare, StringRef(LHSFilenameLower), RHSFilenameLower);
+        if (Cmp != 0)
           return Cmp < 0;
       }
-
-      return (Includes[LHSI].Filename.*Compare)(Includes[RHSI].Filename) < 0;
+      return std::invoke(Compare, Includes[LHSI].Filename,
+                         Includes[RHSI].Filename) < 0;
     });
   }
 



More information about the cfe-commits mailing list