[clang] [clang-format] Add KwBreakBeforeCaseLabel (PR #227668)

via cfe-commits cfe-commits at lists.llvm.org
Wed Sep 30 04:52:08 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-clang-format

Author: Anan Yablonko (ananski-the-3rd)

<details>
<summary>Changes</summary>

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.


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


8 Files Affected:

- (modified) clang/docs/ClangFormatStyleOptions.md (+17) 
- (modified) clang/include/clang/Format/Format.h (+15) 
- (modified) clang/lib/Format/Format.cpp (+57) 
- (modified) clang/lib/Format/UnwrappedLineFormatter.cpp (+8) 
- (modified) clang/lib/Format/UnwrappedLineParser.cpp (+18-1) 
- (modified) clang/lib/Format/UnwrappedLineParser.h (+1) 
- (modified) clang/unittests/Format/ConfigParseTest.cpp (+1) 
- (modified) clang/unittests/Format/FormatTest.cpp (+53) 


``````````diff
diff --git a/clang/docs/ClangFormatStyleOptions.md b/clang/docs/ClangFormatStyleOptions.md
index 81984ff185e53..ae50a75886a84 100644
--- a/clang/docs/ClangFormatStyleOptions.md
+++ b/clang/docs/ClangFormatStyleOptions.md
@@ -5431,6 +5431,23 @@ the configuration (without a prefix: `Auto`).
   newlines. (See
   www.gnu.org/prep/standards/html_node/Formatting.html#:~:text=formfeed.)
 
+(kwbreakbeforecaselabel)=
+
+**KwBreakBeforeCaseLabel** (`Boolean`) {ref}`¶ <KwBreakBeforeCaseLabel>`
+
+: Keep the break keyword and the following case label on the same line.
+
+  ```c++
+  false:                         true:
+  switch (foo) {                 switch (foo) {
+    case 1:                        case 1:
+      bar();                        bar();
+      break;                        break; case 2:
+    case 2:                         bar();
+      bar();                       }
+  }
+  ```
+
 (lambdabodyindentation)=
 
 **LambdaBodyIndentation** (`LambdaBodyIndentationKind`) {versionbadge}`clang-format 13` {ref}`¶ <LambdaBodyIndentation>`
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 6d4fa6e8ee2a7..1574585439971 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -3791,6 +3791,20 @@ struct FormatStyle {
   /// \version 20
   bool KeepFormFeed;
 
+  /// Keep the break keyword and the following case label on the same line.
+  ///
+  /// \code
+  ///    false:                         true:
+  ///    switch (foo) {                 switch (foo) {
+  ///      case 1:                        case 1:
+  ///        bar();                        bar();
+  ///        break;                        break; case 2:
+  ///      case 2:                         bar();
+  ///        bar();                       }
+  ///    }
+  /// \endcode
+  bool KwBreakBeforeCaseLabel;
+
   /// Indentation logic for lambda bodies.
   enum LambdaBodyIndentationKind : int8_t {
     /// Align lambda body relative to the lambda signature. This is the default.
@@ -6259,6 +6273,7 @@ struct FormatStyle {
            JavaScriptWrapImports == R.JavaScriptWrapImports &&
            KeepEmptyLines == R.KeepEmptyLines &&
            KeepFormFeed == R.KeepFormFeed && Language == R.Language &&
+           KwBreakBeforeCaseLabel == R.KwBreakBeforeCaseLabel &&
            LambdaBodyIndentation == R.LambdaBodyIndentation &&
            LineEnding == R.LineEnding && MacroBlockBegin == R.MacroBlockBegin &&
            MacroBlockEnd == R.MacroBlockEnd && Macros == R.Macros &&
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 4c78c1dbe9f80..4f73b52786bdf 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -1418,6 +1418,7 @@ template <> struct MappingTraits<FormatStyle> {
     IO.mapOptional("JavaScriptWrapImports", Style.JavaScriptWrapImports);
     IO.mapOptional("KeepEmptyLines", Style.KeepEmptyLines);
     IO.mapOptional("KeepFormFeed", Style.KeepFormFeed);
+    IO.mapOptional("KwBreakBeforeCaseLabel", Style.KwBreakBeforeCaseLabel);
     IO.mapOptional("LambdaBodyIndentation", Style.LambdaBodyIndentation);
     IO.mapOptional("LineEnding", Style.LineEnding);
     IO.mapOptional("MacroBlockBegin", Style.MacroBlockBegin);
@@ -1998,6 +1999,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) {
       /*AtStartOfFile=*/true,
   };
   LLVMStyle.KeepFormFeed = false;
+  LLVMStyle.KwBreakBeforeCaseLabel = false;
   LLVMStyle.LambdaBodyIndentation = FormatStyle::LBI_Signature;
   LLVMStyle.Language = Language;
   LLVMStyle.LineEnding = FormatStyle::LE_DeriveLF;
@@ -2794,6 +2796,53 @@ class BracesRemover : public TokenAnalyzer {
   }
 };
 
+class KwBreakInserter : public TokenAnalyzer {
+public:
+  KwBreakInserter(const Environment &Env, const FormatStyle &Style)
+      : TokenAnalyzer(Env, Style) {}
+
+  std::pair<tooling::Replacements, unsigned>
+  analyze(TokenAnnotator &Annotator,
+          SmallVectorImpl<AnnotatedLine *> &AnnotatedLines,
+          FormatTokenLexer &Tokens) override {
+    AffectedRangeMgr.computeAffectedLines(AnnotatedLines);
+    tooling::Replacements Result;
+    insertKwBreak(AnnotatedLines, Result);
+    return {Result, 0};
+  }
+
+private:
+  static constexpr StringRef KwBreak = "break;";
+
+  void insertKwBreak(SmallVectorImpl<AnnotatedLine *> &Lines,
+                     tooling::Replacements &Result) {
+
+    const auto &SourceMgr = Env.getSourceManager();
+    const auto *End = Lines.end();
+    for (const auto *I = Lines.begin(); I != End; ++I) {
+      const auto &Line = *I;
+      if (!Line->Children.empty())
+        insertKwBreak(Line->Children, Result);
+      if (!Line->Affected || !Line->startsWith(tok::kw_case))
+        continue;
+
+      const AnnotatedLine *Prev = nullptr;
+      for (auto IReverse = std::make_reverse_iterator(I);
+           IReverse != Lines.rend(); ++IReverse) {
+        if (!(*IReverse)->isComment()) {
+          Prev = *IReverse;
+          break;
+        }
+      }
+      if (!Prev || Prev->getLastNonComment()->isNot(tok::l_brace))
+        continue;
+      cantFail(Result.add(tooling::Replacement(
+          SourceMgr, Line->getFirstNonComment()->Tok.getLocation(), 0,
+          KwBreak)));
+    }
+  }
+};
+
 class SemiRemover : public TokenAnalyzer {
 public:
   SemiRemover(const Environment &Env, const FormatStyle &Style)
@@ -4408,6 +4457,14 @@ reformat(const FormatStyle &Style, StringRef Code,
       });
     }
 
+    if (Style.KwBreakBeforeCaseLabel) {
+      FormatStyle S = Expanded;
+      S.KwBreakBeforeCaseLabel = true;
+      Passes.emplace_back([&, S = std::move(S)](const Environment &Env) {
+        return KwBreakInserter(Env, S).process(/*SkipAnnotation=*/true);
+      });
+    }
+
     if (Style.EnumTrailingComma != FormatStyle::ETC_Leave) {
       Passes.emplace_back([&](const Environment &Env) {
         return EnumTrailingCommaEditor(Env, Expanded)
diff --git a/clang/lib/Format/UnwrappedLineFormatter.cpp b/clang/lib/Format/UnwrappedLineFormatter.cpp
index f005f228328ce..43634e4d1d958 100644
--- a/clang/lib/Format/UnwrappedLineFormatter.cpp
+++ b/clang/lib/Format/UnwrappedLineFormatter.cpp
@@ -281,6 +281,14 @@ class LineJoiner {
         return MergedLines;
     }
 
+    // Handle case labels preceded by a break statement
+    if (Style.KwBreakBeforeCaseLabel &&
+        NextLine.First->isOneOf(tok::kw_case, tok::kw_default) &&
+        TheLine->endsWith(tok::semi, tok::kw_break) &&
+        TheLine->Level <= NextLine.Level) {
+      return 1;
+    }
+
     const auto *PreviousLine = I != AnnotatedLines.begin() ? I[-1] : nullptr;
 
     // Handle blocks where the brace has already been wrapped.
diff --git a/clang/lib/Format/UnwrappedLineParser.cpp b/clang/lib/Format/UnwrappedLineParser.cpp
index 4825e825af1fa..b8b025d9d7b1e 100644
--- a/clang/lib/Format/UnwrappedLineParser.cpp
+++ b/clang/lib/Format/UnwrappedLineParser.cpp
@@ -344,6 +344,17 @@ bool UnwrappedLineParser::precededByCommentOrPPDirective() const {
          (Previous->IsMultiline || Previous->NewlinesBefore > 0);
 }
 
+bool UnwrappedLineParser::linePrecededByKwBreak() const {
+  if (Lines.empty() || Lines.back().Tokens.empty())
+    return false;
+  const UnwrappedLine &PreviousLine = Lines.back();
+
+  if (PreviousLine.Level > Line->Level)
+    return false;
+
+  return PreviousLine.Tokens.front().Tok->is(tok::kw_break);
+}
+
 /// Parses a level, that is ???.
 /// \param OpeningBrace Opening brace (\p nullptr if absent) of that level.
 /// \param IfKind The \p if statement kind in the level.
@@ -3472,6 +3483,11 @@ void UnwrappedLineParser::parseLabel(bool IsGotoLabel) {
   const auto OldLineLevel = Line->Level;
   auto &Level = Line->Level;
 
+  if (!IsGotoLabel && Style.KwBreakBeforeCaseLabel && linePrecededByKwBreak()) {
+    assert(OldLineLevel > 0);
+    Lines.back().Level = OldLineLevel - 1;
+  }
+
   if (IsGotoLabel && IndentGotoLabel == FormatStyle::IGLS_NoIndent)
     Level = 0;
 
@@ -3488,7 +3504,8 @@ void UnwrappedLineParser::parseLabel(bool IsGotoLabel) {
     parseBlock();
     if (FormatTok->is(tok::kw_break)) {
       if (Style.BraceWrapping.AfterControlStatement ==
-          FormatStyle::BWACS_Always) {
+              FormatStyle::BWACS_Always ||
+          Style.KwBreakBeforeCaseLabel) {
         addUnwrappedLine();
         if (!Style.IndentCaseBlocks &&
             Style.BreakBeforeBraces == FormatStyle::BS_Whitesmiths) {
diff --git a/clang/lib/Format/UnwrappedLineParser.h b/clang/lib/Format/UnwrappedLineParser.h
index 5b93c8f346d75..3225e44a064c5 100644
--- a/clang/lib/Format/UnwrappedLineParser.h
+++ b/clang/lib/Format/UnwrappedLineParser.h
@@ -124,6 +124,7 @@ class UnwrappedLineParser {
   void reset();
   void parseFile();
   bool precededByCommentOrPPDirective() const;
+  bool linePrecededByKwBreak() const;
   bool parseLevel(const FormatToken *OpeningBrace = nullptr,
                   IfStmtKind *IfKind = nullptr,
                   FormatToken **IfLeftBrace = nullptr);
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index 86511edb9d40d..0c37acc439239 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -202,6 +202,7 @@ TEST(ConfigParseTest, ParsesConfigurationBools) {
   CHECK_PARSE_BOOL_FIELD(KeepEmptyLines.AtStartOfBlock,
                          "KeepEmptyLinesAtTheStartOfBlocks");
   CHECK_PARSE_BOOL(KeepFormFeed);
+  CHECK_PARSE_BOOL(KwBreakBeforeCaseLabel);
   CHECK_PARSE_BOOL(ObjCSpaceAfterMethodDeclarationPrefix);
   CHECK_PARSE_BOOL(ObjCSpaceAfterProperty);
   CHECK_PARSE_BOOL(ObjCSpaceBeforeProtocolList);
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index bb630da34d7d9..543f717288f77 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -2835,6 +2835,59 @@ TEST_F(FormatTest, FormatsSwitchStatement) {
                Style);
 }
 
+TEST_F(FormatTest, FormatsSwitchStatementKwBreakBeforeCase) {
+  FormatStyle Style = getLLVMStyle();
+  Style.IndentCaseLabels = true;
+  Style.KwBreakBeforeCaseLabel = true;
+
+  verifyFormat("switch (n) {\n"
+               "  break; case 0:;\n"
+               "}",
+               Style);
+
+  verifyFormat("switch (n) {\n"
+               "  break; case 0: {\n"
+               "    return false;\n"
+               "  }\n"
+               "  break; case 1:\n"
+               "  break; default: {\n"
+               "    return true;\n"
+               "  }\n"
+               "}",
+               Style);
+
+  Style.IndentCaseLabels = true;
+  Style.IndentCaseBlocks = false;
+  Style.KwBreakBeforeCaseLabel = true;
+  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"
+               "}",
+               "switch (n) {\n"
+               "case 0: {\n"
+               "  return false;\n"
+               "}\n"
+               "  break;\n"
+               "case 1:\n"
+               "  while (x)\n"
+               "    break;\n"
+               "case 2:\n"
+               "  break;\n"
+               "default: {\n"
+               "  return true;\n"
+               "}\n"
+               "}",
+               Style);
+}
+
 TEST_F(FormatTest, CaseRanges) {
   verifyFormat("switch (x) {\n"
                "case 'A' ... 'Z':\n"

``````````

</details>


https://github.com/llvm/llvm-project/pull/227668


More information about the cfe-commits mailing list