[clang] [clang-format] Add KeepEmptyLines.AtEndOfBlock option (PR #224223)

Arkadiusz Bokowy via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 30 00:22:07 PDT 2026


https://github.com/arkq updated https://github.com/llvm/llvm-project/pull/224223

>From 014d510d55272f29ca0d3d825b93aea0365f3f99 Mon Sep 17 00:00:00 2001
From: Arkadiusz Bokowy <arkadiusz.bokowy at gmail.com>
Date: Thu, 17 Sep 2026 09:47:42 +0200
Subject: [PATCH 1/5] [clang-format] Add KeepEmptyLines.AtEndOfBlock option

Add support for the AtEndOfBlock option in KeepEmptyLines configuration,
which controls whether empty lines are preserved before closing braces.
This feature works in pair with AtStartOfBlock for control of empty
lines at the beginning and end of blocks.

The default is false, which should maintain backward compatibility by
removing empty lines before closing braces. When set to true, empty lines
before closing braces are preserved respecting MaxEmptyLinesToKeep limit.

Closes #22073 and #171003
---
 clang/docs/ClangFormatStyleOptions.md       | 11 +++++
 clang/include/clang/Format/Format.h         | 13 +++++-
 clang/lib/Format/ContinuationIndenter.cpp   |  6 ++-
 clang/lib/Format/Format.cpp                 |  2 +
 clang/lib/Format/UnwrappedLineFormatter.cpp |  3 +-
 clang/unittests/Format/ConfigParseTest.cpp  |  1 +
 clang/unittests/Format/FormatTest.cpp       | 47 +++++++++++++++++++++
 7 files changed, 79 insertions(+), 4 deletions(-)

diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md
index 81984ff185e53..cd3c7b60391a0 100644
--- a/clang/docs/ClangFormatStyleOptions.md
+++ b/clang/docs/ClangFormatStyleOptions.md
@@ -5388,11 +5388,22 @@ the configuration (without a prefix: `Auto`).
 
   ```c++
   KeepEmptyLines:
+    AtEndOfBlock: false
     AtEndOfFile: false
     AtStartOfBlock: false
     AtStartOfFile: false
   ```
 
+  - `bool AtEndOfBlock` Keep empty lines at end of a block.
+
+    ```c++
+    true:                                  false:
+    if (foo) {                     vs.     if (foo) {
+      bar();                                 bar();
+                                           }
+    }
+    ```
+
   - `bool AtEndOfFile` Keep empty lines at end of file.
 
   - `bool AtStartOfBlock` Keep empty lines at start of a block.
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index baf56a9937957..a5b6e76d06b08 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -3750,11 +3750,21 @@ struct FormatStyle {
   ///
   /// \code
   ///   KeepEmptyLines:
+  ///     AtEndOfBlock: false
   ///     AtEndOfFile: false
   ///     AtStartOfBlock: false
   ///     AtStartOfFile: false
   /// \endcode
   struct KeepEmptyLinesStyle {
+    /// Keep empty lines at end of a block.
+    /// \code
+    ///    true:                                  false:
+    ///    if (foo) {                     vs.     if (foo) {
+    ///      bar();                                 bar();
+    ///                                           }
+    ///    }
+    /// \endcode
+    bool AtEndOfBlock;
     /// Keep empty lines at end of file.
     bool AtEndOfFile;
     /// Keep empty lines at start of a block.
@@ -3769,7 +3779,8 @@ struct FormatStyle {
     /// Keep empty lines at start of file.
     bool AtStartOfFile;
     bool operator==(const KeepEmptyLinesStyle &R) const {
-      return AtEndOfFile == R.AtEndOfFile &&
+      return AtEndOfBlock == R.AtEndOfBlock &&
+             AtEndOfFile == R.AtEndOfFile &&
              AtStartOfBlock == R.AtStartOfBlock &&
              AtStartOfFile == R.AtStartOfFile;
     }
diff --git a/clang/lib/Format/ContinuationIndenter.cpp b/clang/lib/Format/ContinuationIndenter.cpp
index aa264c1487bce..97efbc2e6043a 100644
--- a/clang/lib/Format/ContinuationIndenter.cpp
+++ b/clang/lib/Format/ContinuationIndenter.cpp
@@ -1309,8 +1309,10 @@ unsigned ContinuationIndenter::addTokenOnNewLine(LineState &State,
         !Current.MatchingParen->Children.empty()) {
       // lambdas and arrow functions are expressions, thus their r_brace is not
       // on its own line, and thus not covered by UnwrappedLineFormatter's logic
-      // about removing empty lines on closing blocks. Special case them here.
-      MaxEmptyLinesToKeep = 1;
+      // about removing empty lines on closing blocks. Special case them here
+      // with an exception if the KeepEmptyLines.AtEndOfBlock is used.
+      if (!Style.KeepEmptyLines.AtEndOfBlock)
+        MaxEmptyLinesToKeep = 1;
     }
     const unsigned Newlines =
         std::max(1u, std::min(Current.NewlinesBefore, MaxEmptyLinesToKeep));
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 4c78c1dbe9f80..c49144f3291de 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -517,6 +517,7 @@ template <> struct ScalarEnumerationTraits<FormatStyle::JavaScriptQuoteStyle> {
 
 template <> struct MappingTraits<FormatStyle::KeepEmptyLinesStyle> {
   static void mapping(IO &IO, FormatStyle::KeepEmptyLinesStyle &Value) {
+    IO.mapOptional("AtEndOfBlock", Value.AtEndOfBlock);
     IO.mapOptional("AtEndOfFile", Value.AtEndOfFile);
     IO.mapOptional("AtStartOfBlock", Value.AtStartOfBlock);
     IO.mapOptional("AtStartOfFile", Value.AtStartOfFile);
@@ -1993,6 +1994,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.JavaScriptQuotes = FormatStyle::JSQS_Leave;
   LLVMStyle.JavaScriptWrapImports = true;
   LLVMStyle.KeepEmptyLines = {
+      /*AtEndOfBlock=*/false,
       /*AtEndOfFile=*/false,
       /*AtStartOfBlock=*/true,
       /*AtStartOfFile=*/true,
diff --git a/clang/lib/Format/UnwrappedLineFormatter.cpp b/clang/lib/Format/UnwrappedLineFormatter.cpp
index f005f228328ce..41a8aa1f96f05 100644
--- a/clang/lib/Format/UnwrappedLineFormatter.cpp
+++ b/clang/lib/Format/UnwrappedLineFormatter.cpp
@@ -1657,7 +1657,8 @@ static auto computeNewlines(const AnnotatedLine &Line,
       (!RootToken.Next ||
        (RootToken.Next->is(tok::semi) && !RootToken.Next->Next)) &&
       // Do not remove empty lines before namespace closing "}".
-      !getNamespaceToken(&Line, Lines)) {
+      !getNamespaceToken(&Line, Lines) &&
+      !Style.KeepEmptyLines.AtEndOfBlock) {
     Newlines = std::min(Newlines, 1u);
   }
   // Remove empty lines at the start of nested blocks (lambdas/arrow functions)
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 86511edb9d40d..32839712e297a 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -254,6 +254,7 @@ TEST(ConfigParseTest, ParsesConfigurationBools) {
   CHECK_PARSE_NESTED_BOOL(BraceWrapping, SplitEmptyFunction);
   CHECK_PARSE_NESTED_BOOL(BraceWrapping, SplitEmptyRecord);
   CHECK_PARSE_NESTED_BOOL(BraceWrapping, SplitEmptyNamespace);
+  CHECK_PARSE_NESTED_BOOL(KeepEmptyLines, AtEndOfBlock);
   CHECK_PARSE_NESTED_BOOL(KeepEmptyLines, AtEndOfFile);
   CHECK_PARSE_NESTED_BOOL(KeepEmptyLines, AtStartOfBlock);
   CHECK_PARSE_NESTED_BOOL(KeepEmptyLines, AtStartOfFile);
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 5aed37aa56d52..f102c46a05972 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -170,6 +170,7 @@ TEST_F(FormatTest, RemovesEmptyLines) {
   CustomStyle.BreakBeforeBraces = FormatStyle::BS_Custom;
   CustomStyle.BraceWrapping.AfterNamespace = true;
   CustomStyle.KeepEmptyLines.AtStartOfBlock = false;
+  CustomStyle.KeepEmptyLines.AtEndOfBlock = false;
   verifyFormat("namespace N\n"
                "{\n"
                "\n"
@@ -397,6 +398,7 @@ TEST_F(FormatTest, RemovesEmptyLines) {
   Style.BraceWrapping.AfterClass = true;
   Style.BraceWrapping.AfterFunction = true;
   Style.KeepEmptyLines.AtStartOfBlock = false;
+  Style.KeepEmptyLines.AtEndOfBlock = false;
 
   verifyFormat("class Foo\n"
                "{\n"
@@ -25609,6 +25611,51 @@ TEST_F(FormatTest, KeepEmptyLinesAtEOF) {
   verifyFormat(Code, "int i;\n\n\n", Style);
 }
 
+TEST_F(FormatTest, KeepEmptyLinesAtStartOfBlock) {
+  FormatStyle Style = getLLVMStyle();
+  Style.AllowShortFunctionsOnASingleLine =
+      FormatStyle::ShortFunctionStyle::setEmptyAndInline();
+  Style.KeepEmptyLines.AtStartOfBlock = true;
+  Style.MaxEmptyLinesToKeep = 2;
+
+  verifyFormat("void foo() {\n"
+               "\n\n"
+               "  int i;\n"
+               "}",
+               "void foo() {\n"
+               "\n\n\n\n"
+               "  int i;\n"
+               "}",
+               Style);
+}
+
+TEST_F(FormatTest, KeepEmptyLinesAtEndOfBlock) {
+  FormatStyle Style = getLLVMStyle();
+  Style.AllowShortFunctionsOnASingleLine =
+      FormatStyle::ShortFunctionStyle::setEmptyAndInline();
+  Style.KeepEmptyLines.AtEndOfBlock = true;
+  Style.MaxEmptyLinesToKeep = 2;
+
+  verifyFormat("void foo() {\n"
+               "  int i;\n"
+               "\n\n"
+               "}",
+               "void foo() {\n"
+               "  int i;\n"
+               "\n\n\n\n"
+               "}",
+               Style);
+  verifyFormat("foo([]() {\n"
+               "  int i;\n"
+               "\n\n"
+               "});",
+               "foo([]() {\n"
+               "  int i;\n"
+               "\n\n\n\n"
+               "});",
+               Style);
+}
+
 TEST_F(FormatTest, SpaceAfterUDL) {
   verifyFormat("auto c = (4s).count();");
   verifyFormat("auto x = 5s .count() == 5;");

>From ff2c7fe4ca7014349897a0383e49ee12a835732b Mon Sep 17 00:00:00 2001
From: Arkadiusz Bokowy <arkadiusz.bokowy at gmail.com>
Date: Tue, 29 Sep 2026 09:58:34 +0200
Subject: [PATCH 2/5] Keep new lines on new lines for better readability

---
 clang/include/clang/Format/Format.h         |  3 +--
 clang/lib/Format/UnwrappedLineFormatter.cpp |  3 +--
 clang/unittests/Format/FormatTest.cpp       | 21 +++++++++++++++------
 3 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index a5b6e76d06b08..3e22d49d75680 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -3779,8 +3779,7 @@ struct FormatStyle {
     /// Keep empty lines at start of file.
     bool AtStartOfFile;
     bool operator==(const KeepEmptyLinesStyle &R) const {
-      return AtEndOfBlock == R.AtEndOfBlock &&
-             AtEndOfFile == R.AtEndOfFile &&
+      return AtEndOfBlock == R.AtEndOfBlock && AtEndOfFile == R.AtEndOfFile &&
              AtStartOfBlock == R.AtStartOfBlock &&
              AtStartOfFile == R.AtStartOfFile;
     }
diff --git a/clang/lib/Format/UnwrappedLineFormatter.cpp b/clang/lib/Format/UnwrappedLineFormatter.cpp
index 41a8aa1f96f05..11a6d359527ae 100644
--- a/clang/lib/Format/UnwrappedLineFormatter.cpp
+++ b/clang/lib/Format/UnwrappedLineFormatter.cpp
@@ -1657,8 +1657,7 @@ static auto computeNewlines(const AnnotatedLine &Line,
       (!RootToken.Next ||
        (RootToken.Next->is(tok::semi) && !RootToken.Next->Next)) &&
       // Do not remove empty lines before namespace closing "}".
-      !getNamespaceToken(&Line, Lines) &&
-      !Style.KeepEmptyLines.AtEndOfBlock) {
+      !getNamespaceToken(&Line, Lines) && !Style.KeepEmptyLines.AtEndOfBlock) {
     Newlines = std::min(Newlines, 1u);
   }
   // Remove empty lines at the start of nested blocks (lambdas/arrow functions)
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index f102c46a05972..9f7134ea06447 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -25619,11 +25619,14 @@ TEST_F(FormatTest, KeepEmptyLinesAtStartOfBlock) {
   Style.MaxEmptyLinesToKeep = 2;
 
   verifyFormat("void foo() {\n"
-               "\n\n"
+               "\n"
+               "\n"
                "  int i;\n"
                "}",
                "void foo() {\n"
-               "\n\n\n\n"
+               "\n"
+               "\n"
+               "\n"
                "  int i;\n"
                "}",
                Style);
@@ -25638,20 +25641,26 @@ TEST_F(FormatTest, KeepEmptyLinesAtEndOfBlock) {
 
   verifyFormat("void foo() {\n"
                "  int i;\n"
-               "\n\n"
+               "\n"
+               "\n"
                "}",
                "void foo() {\n"
                "  int i;\n"
-               "\n\n\n\n"
+               "\n"
+               "\n"
+               "\n"
                "}",
                Style);
   verifyFormat("foo([]() {\n"
                "  int i;\n"
-               "\n\n"
+               "\n"
+               "\n"
                "});",
                "foo([]() {\n"
                "  int i;\n"
-               "\n\n\n\n"
+               "\n"
+               "\n"
+               "\n"
                "});",
                Style);
 }

>From dce4c7adc5910600589606dafecf0c1ca652baf0 Mon Sep 17 00:00:00 2001
From: Arkadiusz Bokowy <arkadiusz.bokowy at gmail.com>
Date: Tue, 29 Sep 2026 10:13:25 +0200
Subject: [PATCH 3/5] Add release note with KeepEmptyLines.AtEndOfBlock option

---
 clang/docs/ReleaseNotes.md | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 043a0ddae2a6c..109c863910310 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -831,7 +831,8 @@ features cannot lower the translation-unit ABI level;
 - Add `AfterRequiresExpression` sub-option of `BraceWrapping` to wrap the
   body of requires expressions. It is enabled by the `Allman`, `Whitesmiths`,
   and `GNU` styles of `BreakBeforeBraces`.
-
+- Add `AtEndOfBlock` sub-option of `KeepEmptyLines` to control the number of
+  empty lines kept at end of blocks.
 - `QualifierOrder` now supports `typedef`, `consteval`, `constinit`,
   `thread_local`, `extern`, `mutable`, `signed`, `unsigned`, `long`, `short`,
   and `explicit` declaration specifiers.

>From a0d20fa05c645be1dfacd87818cbf377723ee348 Mon Sep 17 00:00:00 2001
From: Arkadiusz Bokowy <arkadiusz.bokowy at gmail.com>
Date: Tue, 29 Sep 2026 23:26:31 +0200
Subject: [PATCH 4/5] Remove unnecessary test for KeepEmptyLinesAtStartOfBlock

---
 clang/unittests/Format/FormatTest.cpp | 21 ---------------------
 1 file changed, 21 deletions(-)

diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 9f7134ea06447..4c007aa0bdca9 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -25611,27 +25611,6 @@ TEST_F(FormatTest, KeepEmptyLinesAtEOF) {
   verifyFormat(Code, "int i;\n\n\n", Style);
 }
 
-TEST_F(FormatTest, KeepEmptyLinesAtStartOfBlock) {
-  FormatStyle Style = getLLVMStyle();
-  Style.AllowShortFunctionsOnASingleLine =
-      FormatStyle::ShortFunctionStyle::setEmptyAndInline();
-  Style.KeepEmptyLines.AtStartOfBlock = true;
-  Style.MaxEmptyLinesToKeep = 2;
-
-  verifyFormat("void foo() {\n"
-               "\n"
-               "\n"
-               "  int i;\n"
-               "}",
-               "void foo() {\n"
-               "\n"
-               "\n"
-               "\n"
-               "  int i;\n"
-               "}",
-               Style);
-}
-
 TEST_F(FormatTest, KeepEmptyLinesAtEndOfBlock) {
   FormatStyle Style = getLLVMStyle();
   Style.AllowShortFunctionsOnASingleLine =

>From cfdc0e904cb005921cef1f040c086fd102b492ab Mon Sep 17 00:00:00 2001
From: Arkadiusz Bokowy <arkadiusz.bokowy at gmail.com>
Date: Wed, 30 Sep 2026 09:20:56 +0200
Subject: [PATCH 5/5] Remove unnecessary change in the unit test

---
 clang/unittests/Format/FormatTest.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 4c007aa0bdca9..09da9273a36bb 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -398,7 +398,6 @@ TEST_F(FormatTest, RemovesEmptyLines) {
   Style.BraceWrapping.AfterClass = true;
   Style.BraceWrapping.AfterFunction = true;
   Style.KeepEmptyLines.AtStartOfBlock = false;
-  Style.KeepEmptyLines.AtEndOfBlock = false;
 
   verifyFormat("class Foo\n"
                "{\n"



More information about the cfe-commits mailing list