[clang] 490018a - [clang-format] Add KwBreakBeforeCaseLabel (#227668)

via cfe-commits cfe-commits at lists.llvm.org
Thu Oct 22 12:54:13 PDT 2026


Author: Anan Yablonko
Date: 2026-10-22T19:50:42Z
New Revision: 490018aa47a8354dc05119f0b142ab0c1bb6cbe7

URL: https://github.com/llvm/llvm-project/commit/490018aa47a8354dc05119f0b142ab0c1bb6cbe7
DIFF: https://github.com/llvm/llvm-project/commit/490018aa47a8354dc05119f0b142ab0c1bb6cbe7.diff

LOG: [clang-format] Add KwBreakBeforeCaseLabel (#227668)

Requesting to add a new option `KwBreakBeforeCaseLabel` to clang-format.

When enabled, switch case labels are styled as `break; case x:`.

For example:

```c
switch (n) {
  case 0:
    foo();
    break;
  default:
    bar();
}
```

becomes:

```c
switch (n) {
  break; case 0:
    foo();
  break; default:
    bar();
}
```

I realize this is a niche formatting choice, but IMO it provides better
readability for the very common case of not using fallthrough in a
switch statement.

Testing:

* clang/unittests/Format/FormatTest.cpp
* clang/unittests/Format/ConfigParseTest.cpp

AI usage disclaimer:
* code is hand-written.
* ChatGPT & Claude used for code-review, researching (LLVM standards,
trying to find a precedent PR if any) and some code understanding.

---------

Co-authored-by: anan.yablonko <anan.yablonko at skillz-edu.org>

Added: 
    

Modified: 
    clang/docs/ClangFormatStyleOptions.md
    clang/include/clang/Format/Format.h
    clang/lib/Format/Format.cpp
    clang/lib/Format/FormatToken.cpp
    clang/lib/Format/FormatToken.h
    clang/lib/Format/TokenAnnotator.cpp
    clang/lib/Format/UnwrappedLineFormatter.cpp
    clang/lib/Format/UnwrappedLineParser.cpp
    clang/unittests/Format/ConfigParseTest.cpp
    clang/unittests/Format/FormatTest.cpp

Removed: 
    


################################################################################
diff  --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md
index af81eed1c6f138..8e9ecbbc09c7df 100644
--- a/clang/docs/ClangFormatStyleOptions.md
+++ b/clang/docs/ClangFormatStyleOptions.md
@@ -4208,6 +4208,23 @@ the configuration (without a prefix: `Auto`).
   #include <vector> // FOOBAR pragma: keep
   ```
 
+(compactcaselabels)=
+
+**CompactCaseLabels** (`Boolean`) {versionbadge}`clang-format 24` {ref}`¶ <CompactCaseLabels>`
+
+: Keep the break keyword and the following case label on the same line.
+
+  ```c++
+  false:                       true:
+  switch (n) {      vs.        switch (n) {
+    case 1:                      case 1:
+      foo();                       foo();
+      break;                     break; case 2:
+    case 2:                        bar();
+      bar();                   }
+  }
+  ```
+
 (compactnamespaces)=
 
 **CompactNamespaces** (`Boolean`) {versionbadge}`clang-format 5` {ref}`¶ <CompactNamespaces>`

diff  --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 9cc887d613595c..69a208559086e6 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -2838,6 +2838,21 @@ struct FormatStyle {
   /// \version 19
   BreakTemplateDeclarationsStyle BreakTemplateDeclarations;
 
+  /// Keep the break keyword and the following case label on the same line.
+  ///
+  /// \code
+  ///    false:                       true:
+  ///    switch (n) {      vs.        switch (n) {
+  ///      case 1:                      case 1:
+  ///        foo();                       foo();
+  ///        break;                     break; case 2:
+  ///      case 2:                        bar();
+  ///        bar();                   }
+  ///    }
+  /// \endcode
+  /// \version 24
+  bool CompactCaseLabels;
+
   /// If `true`, consecutive namespace declarations will be on the same
   /// line. If `false`, each namespace is declared on a new line.
   /// \code
@@ -6246,6 +6261,7 @@ struct FormatStyle {
            BreakStringLiterals == R.BreakStringLiterals &&
            BreakTemplateDeclarations == R.BreakTemplateDeclarations &&
            ColumnLimit == R.ColumnLimit && CommentPragmas == R.CommentPragmas &&
+           CompactCaseLabels == R.CompactCaseLabels &&
            CompactNamespaces == R.CompactNamespaces &&
            ConstructorInitializerIndentWidth ==
                R.ConstructorInitializerIndentWidth &&

diff  --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index c4baf8002c274d..d83ca4a558b7f8 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1392,6 +1392,7 @@ template <> struct MappingTraits<FormatStyle> {
                    Style.BreakTemplateDeclarations);
     IO.mapOptional("ColumnLimit", Style.ColumnLimit);
     IO.mapOptional("CommentPragmas", Style.CommentPragmas);
+    IO.mapOptional("CompactCaseLabels", Style.CompactCaseLabels);
     IO.mapOptional("CompactNamespaces", Style.CompactNamespaces);
     IO.mapOptional("ConstructorInitializerIndentWidth",
                    Style.ConstructorInitializerIndentWidth);
@@ -1970,6 +1971,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
   LLVMStyle.BreakTemplateDeclarations = FormatStyle::BTDS_MultiLine;
   LLVMStyle.ColumnLimit = 80;
   LLVMStyle.CommentPragmas = "^ IWYU pragma:";
+  LLVMStyle.CompactCaseLabels = false;
   LLVMStyle.CompactNamespaces = false;
   LLVMStyle.ConstructorInitializerIndentWidth = 4;
   LLVMStyle.ContinuationIndentWidth = 4;

diff  --git a/clang/lib/Format/FormatToken.cpp b/clang/lib/Format/FormatToken.cpp
index 33e7727f0162d9..c6c051dbfdce29 100644
--- a/clang/lib/Format/FormatToken.cpp
+++ b/clang/lib/Format/FormatToken.cpp
@@ -79,6 +79,17 @@ bool FormatToken::isBlockIndentedInitRBrace(const FormatStyle &Style) const {
   return false;
 }
 
+bool FormatToken::mustBreakAfterAttributes(const FormatStyle &Style) const {
+  switch (Style.BreakAfterAttributes) {
+  case FormatStyle::ABS_Always:
+    return true;
+  case FormatStyle::ABS_Never:
+    return false;
+  default: // ABS_Leave and ABS_LeaveAll
+    return NewlinesBefore > 0;
+  }
+}
+
 bool FormatToken::opensBlockOrBlockTypeList(const FormatStyle &Style) const {
   // C# Does not indent object initialisers as continuations.
   if (is(tok::l_brace) && getBlockKind() == BK_BracedInit && Style.isCSharp())

diff  --git a/clang/lib/Format/FormatToken.h b/clang/lib/Format/FormatToken.h
index 691ecdd24f448f..5bf0d4dfd3292c 100644
--- a/clang/lib/Format/FormatToken.h
+++ b/clang/lib/Format/FormatToken.h
@@ -934,6 +934,10 @@ struct FormatToken {
   /// Returns \c true if this token ends a block indented initializer list.
   [[nodiscard]] bool isBlockIndentedInitRBrace(const FormatStyle &Style) const;
 
+  /// Returns \c true if a newline should exist between this token and a
+  /// preceding attribute.
+  [[nodiscard]] bool mustBreakAfterAttributes(const FormatStyle &Style) const;
+
   /// Returns \c true if this tokens starts a block-type list, i.e. a
   /// list that should be indented with a block indent.
   [[nodiscard]] bool opensBlockOrBlockTypeList(const FormatStyle &Style) const;

diff  --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp
index 8ca8477a285971..c6a87980264a4c 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -22,19 +22,6 @@
 
 namespace clang {
 namespace format {
-
-static bool mustBreakAfterAttributes(const FormatToken &Tok,
-                                     const FormatStyle &Style) {
-  switch (Style.BreakAfterAttributes) {
-  case FormatStyle::ABS_Always:
-    return true;
-  case FormatStyle::ABS_Never:
-    return false;
-  default: // ABS_Leave and ABS_LeaveAll
-    return Tok.NewlinesBefore > 0;
-  }
-}
-
 namespace {
 
 /// Returns \c true if the line starts with a token that can start a statement
@@ -1334,7 +1321,7 @@ class AnnotatingParser {
       if (Prev && Prev->is(TT_AttributeRSquare) &&
           CurrentToken->isOneOf(tok::kw_if, tok::kw_switch, tok::kw_case,
                                 tok::kw_default, tok::kw_for, tok::kw_while) &&
-          mustBreakAfterAttributes(*CurrentToken, Style)) {
+          CurrentToken->mustBreakAfterAttributes(Style)) {
         CurrentToken->MustBreakBefore = true;
       }
     }
@@ -4206,7 +4193,7 @@ void TokenAnnotator::calculateFormattingInformation(AnnotatedLine &Line) const {
     }
 
     if (SeenName && AfterLastAttribute &&
-        mustBreakAfterAttributes(*AfterLastAttribute, Style)) {
+        AfterLastAttribute->mustBreakAfterAttributes(Style)) {
       AfterLastAttribute->MustBreakBefore = true;
       if (LineIsFunctionDeclaration)
         Line.ReturnTypeWrapped = true;

diff  --git a/clang/lib/Format/UnwrappedLineFormatter.cpp b/clang/lib/Format/UnwrappedLineFormatter.cpp
index 2be02c8eea36a7..d1f68877ef0339 100644
--- a/clang/lib/Format/UnwrappedLineFormatter.cpp
+++ b/clang/lib/Format/UnwrappedLineFormatter.cpp
@@ -290,6 +290,20 @@ class LineJoiner {
         return MergedLines;
     }
 
+    // Optionally merge preceding `break` or `[[fallthrough]]` with case label
+    // when at compatible levels, preserving label's indent.
+    if (Style.CompactCaseLabels &&
+        NextLine.First->isOneOf(tok::kw_case, tok::kw_default) &&
+        TheLine->Level <= NextLine.Level + 1 &&
+        canJoinWithCaseLabel(TheLine, *NextLine.First)) {
+      IndentTracker.nextLine(NextLine);
+      unsigned MergedLinesInCaseBody =
+          Style.AllowShortCaseLabelsOnASingleLine
+              ? tryMergeShortCaseLabels(I + 1, E, Limit)
+              : 0;
+      return 1 + MergedLinesInCaseBody;
+    }
+
     const auto *PreviousLine = I != AnnotatedLines.begin() ? I[-1] : nullptr;
 
     // Handle blocks where the brace has already been wrapped.
@@ -672,6 +686,19 @@ class LineJoiner {
     return 0;
   }
 
+  bool canJoinWithCaseLabel(const AnnotatedLine *Line,
+                            const FormatToken &LabelTok) {
+    if (Line->endsWith(tok::semi, tok::kw_break))
+      return true;
+
+    if (Line->startsWith(TT_AttributeLSquare) &&
+        Line->endsWith(tok::semi, TT_AttributeRSquare)) {
+      return !LabelTok.mustBreakAfterAttributes(Style);
+    }
+
+    return false;
+  }
+
   unsigned
   tryMergeSimplePPDirective(ArrayRef<AnnotatedLine *>::const_iterator I,
                             ArrayRef<AnnotatedLine *>::const_iterator E,
@@ -841,10 +868,16 @@ class LineJoiner {
           EndsWithComment) {
         return 0;
       }
+      const auto *J = I + 2 + NumStmts;
+      if (Style.CompactCaseLabels && J != E &&
+          (*J)->First->isOneOf(tok::kw_case, tok::kw_default) &&
+          canJoinWithCaseLabel(Line, *(*J)->First)) {
+        break;
+      }
+
       if (Line->First->is(tok::comment)) {
         if (Level != Line->Level)
           return 0;
-        const auto *J = I + 2 + NumStmts;
         for (; J != E; ++J) {
           Line = *J;
           if (Line->InPPDirective != InPPDirective)

diff  --git a/clang/lib/Format/UnwrappedLineParser.cpp b/clang/lib/Format/UnwrappedLineParser.cpp
index 1b00e9015ea6e0..c4e7faa055135b 100644
--- a/clang/lib/Format/UnwrappedLineParser.cpp
+++ b/clang/lib/Format/UnwrappedLineParser.cpp
@@ -3563,7 +3563,8 @@ void UnwrappedLineParser::parseLabel(bool IsGotoLabel) {
     parseBlock();
     if (FormatTok->is(tok::kw_break)) {
       if (Style.BraceWrapping.AfterControlStatement ==
-          FormatStyle::BWACS_Always) {
+              FormatStyle::BWACS_Always ||
+          Style.CompactCaseLabels) {
         addUnwrappedLine();
         if (!Style.IndentCaseBlocks &&
             Style.BreakBeforeBraces == FormatStyle::BS_Whitesmiths) {

diff  --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index ab160db1e9f13c..0ca02058a24948 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -185,6 +185,7 @@ TEST(ConfigParseTest, ParsesConfigurationBools) {
   CHECK_PARSE_BOOL(BreakBeforeTernaryOperators);
   CHECK_PARSE_BOOL(BreakFunctionDeclarationParameters);
   CHECK_PARSE_BOOL(BreakStringLiterals);
+  CHECK_PARSE_BOOL(CompactCaseLabels);
   CHECK_PARSE_BOOL(CompactNamespaces);
   CHECK_PARSE_BOOL(DerivePointerAlignment);
   CHECK_PARSE_BOOL_FIELD(DerivePointerAlignment, "DerivePointerBinding");

diff  --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 8a2c54ec5999df..156a2eff27a712 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -2833,6 +2833,117 @@ TEST_F(FormatTest, FormatsSwitchStatement) {
                Style);
 }
 
+TEST_F(FormatTest, FormatsSwitchStatementCompactCaseLabels) {
+  FormatStyle Style = getLLVMStyle();
+  Style.CompactCaseLabels = true;
+
+  Style.IndentCaseLabels = false;
+  verifyFormat("switch (n) {\n"
+               "break; case 0: {\n"
+               "  return false;\n"
+               "}\n"
+               "break; default: {\n"
+               "  return true;\n"
+               "}\n"
+               "}",
+               Style);
+
+  Style.IndentCaseLabels = true;
+  verifyFormat("switch (n) {\n"
+               "  break; case 0:\n"
+               "    foo();\n"
+               "  [[fallthrough]]; case 1:\n"
+               "    bar();\n"
+               "}",
+               Style);
+
+  verifyFormat("switch (n) {\n"
+               "  break; case 0: {\n"
+               "    return false;\n"
+               "  }\n"
+               "  [[fallthrough]]; default:;\n"
+               "}",
+               Style);
+
+  verifyFormat("switch (n) {\n"
+               "  break; case 0: {\n"
+               "    return false;\n"
+               "  }\n"
+               "  break; case 1:\n"
+               "    while (x)\n"
+               "      break;\n"
+               "  case 2:\n"
+               "  break; default: {\n"
+               "    return true;\n"
+               "  }\n"
+               "}",
+               Style);
+
+  Style.BreakBeforeBraces = FormatStyle::BS_Custom;
+  Style.BraceWrapping.AfterControlStatement = FormatStyle::BWACS_Always;
+  Style.BraceWrapping.AfterCaseLabel = true;
+  verifyFormat("switch (n)\n"
+               "{\n"
+               "  break; case 0:\n"
+               "  {\n"
+               "    foo();\n"
+               "  }\n"
+               "  break; case 1:\n"
+               "  {\n"
+               "    bar();\n"
+               "  }\n"
+               "}",
+               Style);
+
+  Style.BraceWrapping.IndentBraces = true;
+  verifyFormat("switch (n)\n"
+               "  {\n"
+               "    break; case 0:\n"
+               "      {\n"
+               "        foo();\n"
+               "      }\n"
+               "    break; case 1:\n"
+               "      {\n"
+               "        bar();\n"
+               "      }\n"
+               "  }",
+               Style);
+
+  Style.BraceWrapping.AfterControlStatement = FormatStyle::BWACS_Never;
+  Style.BraceWrapping.AfterCaseLabel = false;
+  Style.BraceWrapping.IndentBraces = false;
+
+  Style.BreakAfterAttributes = FormatStyle::ABS_Always;
+  verifyFormat("switch (n) {\n"
+               "  break; case 0:\n"
+               "    foo();\n"
+               "    [[fallthrough]];\n"
+               "  case 1:\n"
+               "    bar();\n"
+               "}",
+               "switch (n) {\n"
+               "  break; case 0:\n"
+               "    foo();\n"
+               "  [[fallthrough]]; case 1:\n"
+               "    bar();\n"
+               "}",
+               Style);
+
+  Style.AllowShortCaseLabelsOnASingleLine = true;
+  Style.AlignConsecutiveShortCaseStatements.Enabled = true;
+  verifyFormat("switch (n) {\n"
+               "  break; case 0:  b = false;\n"
+               "  break; default: b = true;\n"
+               "}",
+               Style);
+
+  verifyFormat("switch (n) {\n"
+               "  case 0:         b = false;\n"
+               "  break; default: b = true;\n"
+               "}",
+               Style);
+}
+
 TEST_F(FormatTest, CaseRanges) {
   verifyFormat("switch (x) {\n"
                "case 'A' ... 'Z':\n"


        


More information about the cfe-commits mailing list