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 <[email protected]> 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" _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
