[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