Author: Sergey Subbotin Date: 2026-02-21T23:08:18+01:00 New Revision: 62e55b410c1de01a9ba0cdd324fd53984e15c7b5
URL: https://github.com/llvm/llvm-project/commit/62e55b410c1de01a9ba0cdd324fd53984e15c7b5 DIFF: https://github.com/llvm/llvm-project/commit/62e55b410c1de01a9ba0cdd324fd53984e15c7b5.diff LOG: [clang-format] Add per-operator granularity for BreakBinaryOperations (#181051) ## Summary Extend `BreakBinaryOperations` to accept a structured YAML configuration with per-operator break rules and minimum chain length gating via `PerOperator`. - **Per-operator rules**: specify break style (`Never`, `OnePerLine`, `RespectPrecedence`) for specific operator groups - **Minimum chain length**: only trigger breaking when a chain has N or more operators - **Fully backward-compatible**: the simple scalar form (`BreakBinaryOperations: OnePerLine`) behaves identically to the current enum value RFC discussion: https://discourse.llvm.org/t/rfc-per-operator-granularity-for-breakbinaryoperations/89800 ### New YAML syntax ```yaml BreakBinaryOperations: Default: Never PerOperator: - Operators: ['&&', '||'] Style: OnePerLine MinChainLength: 3 - Operators: ['|'] Style: OnePerLine ``` ### Motivation `BreakBinaryOperations` operates at the level of all binary operators simultaneously. Enabling `OnePerLine` for `&&`/`||` condition chains also forces it on `+`, `|`, and other operators, which may not be desired. The only workaround today is `// clang-format off`. ## Test plan - [x] All existing `BreakBinaryOperations` unit tests updated and passing - [x] New tests for per-operator rules (`&&`/`||` OnePerLine + default Never) - [x] New tests for multiple operator groups (`&&`/`||` + `|`) - [x] New tests for `MinChainLength` gating (chain of 2 vs 3+) - [x] Config parse tests for structured YAML form - [x] RST documentation auto-generated via `dump_format_style.py` - [x] Release notes updated Added: Modified: clang/docs/ClangFormatStyleOptions.rst clang/docs/ReleaseNotes.rst clang/docs/tools/dump_format_style.py clang/include/clang/Format/Format.h clang/lib/Format/ContinuationIndenter.cpp clang/lib/Format/Format.cpp clang/lib/Format/TokenAnnotator.cpp clang/unittests/Format/ConfigParseTest.cpp clang/unittests/Format/FormatTest.cpp Removed: ################################################################################ diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst index 437f559fc0395..db03a9a8f87ec 100644 --- a/clang/docs/ClangFormatStyleOptions.rst +++ b/clang/docs/ClangFormatStyleOptions.rst @@ -3613,42 +3613,110 @@ the configuration (without a prefix: ``Auto``). .. _BreakBinaryOperations: -**BreakBinaryOperations** (``BreakBinaryOperationsStyle``) :versionbadge:`clang-format 20` :ref:`¶ <BreakBinaryOperations>` +**BreakBinaryOperations** (``BreakBinaryOperationsOptions``) :versionbadge:`clang-format 20` :ref:`¶ <BreakBinaryOperations>` The break binary operations style to use. - Possible values: + Nested configuration flags: - * ``BBO_Never`` (in configuration: ``Never``) - Don't break binary operations + Options for ``BreakBinaryOperations``. - .. code-block:: c++ + If specified as a simple string (e.g. ``OnePerLine``), it behaves like + the original enum and applies to all binary operators. - aaa + bbbb * ccccc - ddddd + - eeeeeeeeeeeeeeee; + If specified as a struct, allows per-operator configuration: - * ``BBO_OnePerLine`` (in configuration: ``OnePerLine``) - Binary operations will either be all on the same line, or each operation - will have one line each. + .. code-block:: yaml - .. code-block:: c++ + BreakBinaryOperations: + Default: Never + PerOperator: + - Operators: ['&&', '||'] + Style: OnePerLine + MinChainLength: 3 - aaa + - bbbb * - ccccc - - ddddd + - eeeeeeeeeeeeeeee; + * ``BreakBinaryOperationsStyle Default`` :versionbadge:`clang-format 23` - * ``BBO_RespectPrecedence`` (in configuration: ``RespectPrecedence``) - Binary operations of a particular precedence that exceed the column - limit will have one line each. + The default break style for operators not covered by ``PerOperator``. - .. code-block:: c++ + Possible values: + + * ``BBO_Never`` (in configuration: ``Never``) + Don't break binary operations + + .. code-block:: c++ + + aaa + bbbb * ccccc - ddddd + + eeeeeeeeeeeeeeee; + + * ``BBO_OnePerLine`` (in configuration: ``OnePerLine``) + Binary operations will either be all on the same line, or each operation + will have one line each. + + .. code-block:: c++ + + aaa + + bbbb * + ccccc - + ddddd + + eeeeeeeeeeeeeeee; + + * ``BBO_RespectPrecedence`` (in configuration: ``RespectPrecedence``) + Binary operations of a particular precedence that exceed the column + limit will have one line each. + + .. code-block:: c++ + + aaa + + bbbb * ccccc - + ddddd + + eeeeeeeeeeeeeeee; + + + * ``List of BinaryOperationBreakRules PerOperator`` Per-operator override rules. + + * ``List of Strings Operators`` :versionbadge:`clang-format 23` The list of operators this rule applies to, e.g. ``&&``, ``||``, ``|``. + Alternative spellings (e.g. ``and`` for ``&&``) are accepted. + + * ``BreakBinaryOperationsStyle Style`` + The break style for these operators (defaults to ``OnePerLine``). + + Possible values: + + * ``BBO_Never`` (in configuration: ``Never``) + Don't break binary operations + + .. code-block:: c++ + + aaa + bbbb * ccccc - ddddd + + eeeeeeeeeeeeeeee; + + * ``BBO_OnePerLine`` (in configuration: ``OnePerLine``) + Binary operations will either be all on the same line, or each operation + will have one line each. + + .. code-block:: c++ + + aaa + + bbbb * + ccccc - + ddddd + + eeeeeeeeeeeeeeee; + + * ``BBO_RespectPrecedence`` (in configuration: ``RespectPrecedence``) + Binary operations of a particular precedence that exceed the column + limit will have one line each. + + .. code-block:: c++ + + aaa + + bbbb * ccccc - + ddddd + + eeeeeeeeeeeeeeee; - aaa + - bbbb * ccccc - - ddddd + - eeeeeeeeeeeeeeee; + * ``unsigned MinChainLength`` Minimum number of operands in a chain before the rule triggers. + For example, ``a && b && c`` is a chain of length 3. + ``0`` means always break (when the line is too long). .. _BreakConstructorInitializers: diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst index 0bbe75b215cad..2bac5e7b06a59 100644 --- a/clang/docs/ReleaseNotes.rst +++ b/clang/docs/ReleaseNotes.rst @@ -408,6 +408,8 @@ clang-format '-'/'+' and the return type in Objective-C method declarations - Add ``AfterComma`` value to ``BreakConstructorInitializers`` to allow breaking constructor initializers after commas, keeping the colon on the same line. +- Extend ``BreakBinaryOperations`` to accept a structured configuration with + per-operator break rules and minimum chain length gating via ``PerOperator``. libclang -------- diff --git a/clang/docs/tools/dump_format_style.py b/clang/docs/tools/dump_format_style.py index f035143f6b3d1..570a0cc6dde1d 100755 --- a/clang/docs/tools/dump_format_style.py +++ b/clang/docs/tools/dump_format_style.py @@ -79,6 +79,8 @@ def to_yaml_type(typestr: str): return "Unsigned" elif typestr == "std::string": return "String" + elif typestr == "tok::TokenKind": + return "String" match = re.match(r"std::vector<(.*)>$", typestr) if match: @@ -152,7 +154,7 @@ def __init__(self, name, comment, version): def __str__(self): if self.version: - return "\n* ``%s`` :versionbadge:`clang-format %s`\n%s" % ( + return "\n* ``%s`` :versionbadge:`clang-format %s` %s" % ( self.name, self.version, doxygen2rst(indent(self.comment, 2, indent_first_line=False)), @@ -399,9 +401,32 @@ class State: ) ) else: - nested_struct.values.append( - NestedField(field_type + " " + field_name, comment, version) - ) + vec_match = re.match(r"std::vector<(.*)>$", field_type) + if vec_match and vec_match.group(1) in nested_structs: + inner_struct = nested_structs[vec_match.group(1)] + display = "List of %ss %s" % ( + vec_match.group(1), + field_name, + ) + nested_struct.values.append( + NestedField(display, comment, version) + ) + nested_struct.values.extend(inner_struct.values) + else: + vec_match = re.match(r"std::vector<(.*)>$", field_type) + if vec_match: + display_type = "List of " + pluralize( + to_yaml_type(vec_match.group(1)) + ) + else: + display_type = field_type + nested_struct.values.append( + NestedField( + display_type + " " + field_name, + comment, + version, + ) + ) version = None elif state == State.InEnum: if line.startswith("///"): diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h index ca0bd47d10613..0f6f4e4de1971 100644 --- a/clang/include/clang/Format/Format.h +++ b/clang/include/clang/Format/Format.h @@ -15,6 +15,7 @@ #define LLVM_CLANG_FORMAT_FORMAT_H #include "clang/Basic/LangOptions.h" +#include "clang/Basic/TokenKinds.h" #include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Inclusions/IncludeStyle.h" #include "llvm/ADT/ArrayRef.h" @@ -2450,9 +2451,84 @@ struct FormatStyle { BBO_RespectPrecedence }; + /// A rule that specifies how to break a specific set of binary operators. + /// \version 23 + struct BinaryOperationBreakRule { + /// The list of operators this rule applies to, e.g. ``&&``, ``||``, ``|``. + /// Alternative spellings (e.g. ``and`` for ``&&``) are accepted. + std::vector<tok::TokenKind> Operators; + /// The break style for these operators (defaults to ``OnePerLine``). + BreakBinaryOperationsStyle Style; + /// Minimum number of operands in a chain before the rule triggers. + /// For example, ``a && b && c`` is a chain of length 3. + /// ``0`` means always break (when the line is too long). + unsigned MinChainLength; + bool operator==(const BinaryOperationBreakRule &R) const { + return Operators == R.Operators && Style == R.Style && + MinChainLength == R.MinChainLength; + } + bool operator!=(const BinaryOperationBreakRule &R) const { + return !(*this == R); + } + }; + + /// Options for ``BreakBinaryOperations``. + /// + /// If specified as a simple string (e.g. ``OnePerLine``), it behaves like + /// the original enum and applies to all binary operators. + /// + /// If specified as a struct, allows per-operator configuration: + /// \code{.yaml} + /// BreakBinaryOperations: + /// Default: Never + /// PerOperator: + /// - Operators: ['&&', '||'] + /// Style: OnePerLine + /// MinChainLength: 3 + /// \endcode + /// \version 23 + struct BreakBinaryOperationsOptions { + /// The default break style for operators not covered by ``PerOperator``. + BreakBinaryOperationsStyle Default; + /// Per-operator override rules. + std::vector<BinaryOperationBreakRule> PerOperator; + const BinaryOperationBreakRule * + findRuleForOperator(tok::TokenKind Kind) const { + for (const auto &Rule : PerOperator) { + if (llvm::find(Rule.Operators, Kind) != Rule.Operators.end()) + return &Rule; + // clang-format splits ">>" into two ">" tokens for template parsing. + // Match ">" against ">>" rules so that per-operator rules for ">>" + // (stream extraction / right shift) work correctly. + if (Kind == tok::greater && + llvm::find(Rule.Operators, tok::greatergreater) != + Rule.Operators.end()) { + return &Rule; + } + } + return nullptr; + } + BreakBinaryOperationsStyle getStyleForOperator(tok::TokenKind Kind) const { + if (const auto *Rule = findRuleForOperator(Kind)) + return Rule->Style; + return Default; + } + unsigned getMinChainLengthForOperator(tok::TokenKind Kind) const { + if (const auto *Rule = findRuleForOperator(Kind)) + return Rule->MinChainLength; + return 0; + } + bool operator==(const BreakBinaryOperationsOptions &R) const { + return Default == R.Default && PerOperator == R.PerOperator; + } + bool operator!=(const BreakBinaryOperationsOptions &R) const { + return !(*this == R); + } + }; + /// The break binary operations style to use. /// \version 20 - BreakBinaryOperationsStyle BreakBinaryOperations; + BreakBinaryOperationsOptions BreakBinaryOperations; /// Different ways to break initializers. enum BreakConstructorInitializersStyle : int8_t { diff --git a/clang/lib/Format/ContinuationIndenter.cpp b/clang/lib/Format/ContinuationIndenter.cpp index f1edd71df73fa..4137a23230d51 100644 --- a/clang/lib/Format/ContinuationIndenter.cpp +++ b/clang/lib/Format/ContinuationIndenter.cpp @@ -144,14 +144,48 @@ static bool startsNextOperand(const FormatToken &Current) { return isAlignableBinaryOperator(Previous) && !Current.isTrailingComment(); } +// Returns the number of operands in the chain containing \c Op. +// For example, `a && b && c` has 3 operands (and 2 operators). +static unsigned getChainLength(const FormatToken &Op) { + const FormatToken *Last = &Op; + while (Last->NextOperator) + Last = Last->NextOperator; + return Last->OperatorIndex + 2; +} + // Returns \c true if \c Current is a binary operation that must break. static bool mustBreakBinaryOperation(const FormatToken &Current, const FormatStyle &Style) { - return Style.BreakBinaryOperations != FormatStyle::BBO_Never && - Current.CanBreakBefore && - (Style.BreakBeforeBinaryOperators == FormatStyle::BOS_None - ? startsNextOperand - : isAlignableBinaryOperator)(Current); + if (!Current.CanBreakBefore) + return false; + + // Determine the operator token: when breaking after the operator, + // it is Current.Previous; when breaking before, it is Current itself. + bool BreakBefore = Style.BreakBeforeBinaryOperators != FormatStyle::BOS_None; + const FormatToken *OpToken = BreakBefore ? &Current : Current.Previous; + + if (!OpToken) + return false; + + // Check that this is an alignable binary operator. + if (BreakBefore) { + if (!isAlignableBinaryOperator(Current)) + return false; + } else if (!startsNextOperand(Current)) { + return false; + } + + // Look up per-operator rule or fall back to Default. + const auto OperatorBreakStyle = + Style.BreakBinaryOperations.getStyleForOperator(OpToken->Tok.getKind()); + if (OperatorBreakStyle == FormatStyle::BBO_Never) + return false; + + // Check MinChainLength: if the chain is too short, don't force a break. + const unsigned MinChain = + Style.BreakBinaryOperations.getMinChainLengthForOperator( + OpToken->Tok.getKind()); + return MinChain == 0 || getChainLength(*OpToken) >= MinChain; } static bool opensProtoMessageField(const FormatToken &LessTok, diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp index de149edc01a80..2f67ec86b101a 100644 --- a/clang/lib/Format/Format.cpp +++ b/clang/lib/Format/Format.cpp @@ -31,6 +31,8 @@ using clang::format::FormatStyle; LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::RawStringFormat) +LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::BinaryOperationBreakRule) +LLVM_YAML_IS_SEQUENCE_VECTOR(clang::tok::TokenKind) enum BracketAlignmentStyle : int8_t { BAS_Align, @@ -275,6 +277,62 @@ struct ScalarEnumerationTraits<FormatStyle::BreakBinaryOperationsStyle> { } }; +template <> struct ScalarTraits<clang::tok::TokenKind> { + static void output(const clang::tok::TokenKind &Value, void *, + llvm::raw_ostream &Out) { + if (const char *Spelling = clang::tok::getPunctuatorSpelling(Value)) + Out << Spelling; + else + Out << clang::tok::getTokenName(Value); + } + + static StringRef input(StringRef Scalar, void *, + clang::tok::TokenKind &Value) { + // Map operator spelling strings to tok::TokenKind. +#define PUNCTUATOR(Name, Spelling) \ + if (Scalar == Spelling) { \ + Value = clang::tok::Name; \ + return {}; \ + } +#include "clang/Basic/TokenKinds.def" + return "unknown operator"; + } + + static QuotingType mustQuote(StringRef) { return QuotingType::None; } +}; + +template <> struct MappingTraits<FormatStyle::BinaryOperationBreakRule> { + static void mapping(IO &IO, FormatStyle::BinaryOperationBreakRule &Value) { + IO.mapOptional("Operators", Value.Operators); + // Default to OnePerLine since a per-operator rule with Never is a no-op. + if (!IO.outputting()) + Value.Style = FormatStyle::BBO_OnePerLine; + IO.mapOptional("Style", Value.Style); + IO.mapOptional("MinChainLength", Value.MinChainLength); + } +}; + +template <> struct MappingTraits<FormatStyle::BreakBinaryOperationsOptions> { + static void enumInput(IO &IO, + FormatStyle::BreakBinaryOperationsOptions &Value) { + IO.enumCase(Value, "Never", + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_Never, {}})); + IO.enumCase(Value, "OnePerLine", + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_OnePerLine, {}})); + IO.enumCase(Value, "RespectPrecedence", + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_RespectPrecedence, {}})); + } + + static void mapping(IO &IO, + FormatStyle::BreakBinaryOperationsOptions &Value) { + IO.mapOptional("Default", Value.Default); + IO.mapOptional("PerOperator", Value.PerOperator); + } +}; + template <> struct ScalarEnumerationTraits<FormatStyle::BreakConstructorInitializersStyle> { static void @@ -1725,7 +1783,7 @@ FormatStyle getLLVMStyle(FormatStyle::LanguageKind Language) { LLVMStyle.BreakBeforeInlineASMColon = FormatStyle::BBIAS_OnlyMultiline; LLVMStyle.BreakBeforeTemplateCloser = false; LLVMStyle.BreakBeforeTernaryOperators = true; - LLVMStyle.BreakBinaryOperations = FormatStyle::BBO_Never; + LLVMStyle.BreakBinaryOperations = {FormatStyle::BBO_Never, {}}; LLVMStyle.BreakConstructorInitializers = FormatStyle::BCIS_BeforeColon; LLVMStyle.BreakFunctionDefinitionParameters = false; LLVMStyle.BreakInheritanceList = FormatStyle::BILS_BeforeColon; diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp index c66d883b30870..34e81bbc97578 100644 --- a/clang/lib/Format/TokenAnnotator.cpp +++ b/clang/lib/Format/TokenAnnotator.cpp @@ -3276,14 +3276,19 @@ class ExpressionParser { parse(Precedence + 1); int CurrentPrecedence = getCurrentPrecedence(); - if (Style.BreakBinaryOperations == FormatStyle::BBO_OnePerLine && - CurrentPrecedence > prec::Conditional && + if (CurrentPrecedence > prec::Conditional && CurrentPrecedence < prec::PointerToMember) { - // When BreakBinaryOperations is set to BreakAll, - // all operations will be on the same line or on individual lines. - // Override precedence to avoid adding fake parenthesis which could - // group operations of a diff erent precedence level on the same line - CurrentPrecedence = prec::Additive; + // When BreakBinaryOperations is globally OnePerLine (no per-operator + // rules), flatten all precedence levels so that every operator is + // treated equally for line-breaking purposes. With per-operator rules + // we must preserve natural precedence so that higher-precedence + // sub-expressions (e.g. `x << 8` inside a `|` chain) stay grouped; + // mustBreakBinaryOperation() handles the forced breaks instead. + if (Style.BreakBinaryOperations.PerOperator.empty() && + Style.BreakBinaryOperations.Default == + FormatStyle::BBO_OnePerLine) { + CurrentPrecedence = prec::Additive; + } } if (Precedence == CurrentPrecedence && Current && diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp index b45e2eb23765d..f270602f32604 100644 --- a/clang/unittests/Format/ConfigParseTest.cpp +++ b/clang/unittests/Format/ConfigParseTest.cpp @@ -453,13 +453,58 @@ TEST(ConfigParseTest, ParsesConfiguration) { CHECK_PARSE("BreakBeforeBinaryOperators: true", BreakBeforeBinaryOperators, FormatStyle::BOS_All); - Style.BreakBinaryOperations = FormatStyle::BBO_Never; + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; CHECK_PARSE("BreakBinaryOperations: OnePerLine", BreakBinaryOperations, - FormatStyle::BBO_OnePerLine); + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_OnePerLine, {}})); CHECK_PARSE("BreakBinaryOperations: RespectPrecedence", BreakBinaryOperations, - FormatStyle::BBO_RespectPrecedence); - CHECK_PARSE("BreakBinaryOperations: Never", BreakBinaryOperations, - FormatStyle::BBO_Never); + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_RespectPrecedence, {}})); + CHECK_PARSE( + "BreakBinaryOperations: Never", BreakBinaryOperations, + FormatStyle::BreakBinaryOperationsOptions({FormatStyle::BBO_Never, {}})); + + // Structured form + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; + CHECK_PARSE("BreakBinaryOperations:\n" + " Default: OnePerLine", + BreakBinaryOperations, + FormatStyle::BreakBinaryOperationsOptions( + {FormatStyle::BBO_OnePerLine, {}})); + + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; + EXPECT_EQ(0, parseConfiguration("BreakBinaryOperations:\n" + " Default: Never\n" + " PerOperator:\n" + " - Operators: ['&&', '||']\n" + " Style: OnePerLine\n" + " MinChainLength: 3", + &Style) + .value()); + EXPECT_EQ(Style.BreakBinaryOperations.Default, FormatStyle::BBO_Never); + ASSERT_EQ(Style.BreakBinaryOperations.PerOperator.size(), 1u); + std::vector<tok::TokenKind> ExpectedOps = {tok::ampamp, tok::pipepipe}; + EXPECT_EQ(Style.BreakBinaryOperations.PerOperator[0].Operators, ExpectedOps); + EXPECT_EQ(Style.BreakBinaryOperations.PerOperator[0].Style, + FormatStyle::BBO_OnePerLine); + EXPECT_EQ(Style.BreakBinaryOperations.PerOperator[0].MinChainLength, 3u); + + // Parse ">" and ">>" which have edge cases: clang-format splits ">>" into + // two ">" tokens, so ">>" maps to tok::greatergreater while ">" maps to + // tok::greater. + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; + EXPECT_EQ(0, parseConfiguration("BreakBinaryOperations:\n" + " Default: Never\n" + " PerOperator:\n" + " - Operators: ['>', '>>']\n" + " Style: OnePerLine", + &Style) + .value()); + ASSERT_EQ(Style.BreakBinaryOperations.PerOperator.size(), 1u); + std::vector<tok::TokenKind> ExpectedShiftOps = {tok::greater, + tok::greatergreater}; + EXPECT_EQ(Style.BreakBinaryOperations.PerOperator[0].Operators, + ExpectedShiftOps); Style.BreakConstructorInitializers = FormatStyle::BCIS_BeforeColon; CHECK_PARSE("BreakConstructorInitializers: BeforeComma", diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp index d6c49163abbc9..43633b582a8ab 100644 --- a/clang/unittests/Format/FormatTest.cpp +++ b/clang/unittests/Format/FormatTest.cpp @@ -28403,7 +28403,9 @@ TEST_F(FormatTest, SpaceBetweenKeywordAndLiteral) { TEST_F(FormatTest, BreakBinaryOperations) { auto Style = getLLVMStyleWithColumns(60); - EXPECT_EQ(Style.BreakBinaryOperations, FormatStyle::BBO_Never); + FormatStyle::BreakBinaryOperationsOptions ExpectedDefault = { + FormatStyle::BBO_Never, {}}; + EXPECT_EQ(Style.BreakBinaryOperations, ExpectedDefault); // Logical operations verifyFormat("if (condition1 && condition2) {\n" @@ -28442,7 +28444,7 @@ TEST_F(FormatTest, BreakBinaryOperations) { " longOperand_3_;", Style); - Style.BreakBinaryOperations = FormatStyle::BBO_OnePerLine; + Style.BreakBinaryOperations.Default = FormatStyle::BBO_OnePerLine; // Logical operations verifyFormat("if (condition1 && condition2) {\n" @@ -28527,7 +28529,7 @@ TEST_F(FormatTest, BreakBinaryOperations) { " longOperand_3_;", Style); - Style.BreakBinaryOperations = FormatStyle::BBO_RespectPrecedence; + Style.BreakBinaryOperations.Default = FormatStyle::BBO_RespectPrecedence; verifyFormat("result = op1 + op2 * op3 - op4;", Style); verifyFormat("result = operand1 +\n" @@ -28559,7 +28561,7 @@ TEST_F(FormatTest, BreakBinaryOperations) { " longOperand_3_;", Style); - Style.BreakBinaryOperations = FormatStyle::BBO_OnePerLine; + Style.BreakBinaryOperations.Default = FormatStyle::BBO_OnePerLine; Style.BreakBeforeBinaryOperators = FormatStyle::BOS_NonAssignment; // Logical operations @@ -28640,7 +28642,7 @@ TEST_F(FormatTest, BreakBinaryOperations) { " >> longOperand_3_;", Style); - Style.BreakBinaryOperations = FormatStyle::BBO_RespectPrecedence; + Style.BreakBinaryOperations.Default = FormatStyle::BBO_RespectPrecedence; verifyFormat("result = op1 + op2 * op3 - op4;", Style); verifyFormat("result = operand1\n" @@ -28673,6 +28675,112 @@ TEST_F(FormatTest, BreakBinaryOperations) { Style); } +TEST_F(FormatTest, BreakBinaryOperationsPerOperator) { + auto Style = getLLVMStyleWithColumns(60); + + // Per-operator override: && and || are OnePerLine, rest is Never (default). + FormatStyle::BinaryOperationBreakRule LogicalRule; + LogicalRule.Operators = {tok::ampamp, tok::pipepipe}; + LogicalRule.Style = FormatStyle::BBO_OnePerLine; + LogicalRule.MinChainLength = 0; + + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; + Style.BreakBinaryOperations.PerOperator = {LogicalRule}; + + // Logical operators break one-per-line when line is too long. + verifyFormat("bool valid = isConnectionReady() &&\n" + " isSessionNotExpired() &&\n" + " hasRequiredPermission();", + Style); + + // Arithmetic operators stay with default (Never). + verifyFormat("int total = unitBasePrice + shippingCostPerItem +\n" + " applicableTaxAmount + handlingFeePerUnit;", + Style); + + // Short logical chain that fits stays on one line. + verifyFormat("bool x = a && b && c;", Style); + + // Multiple PerOperator groups: && and || plus | operators. + FormatStyle::BinaryOperationBreakRule BitwiseOrRule; + BitwiseOrRule.Operators = {tok::pipe}; + BitwiseOrRule.Style = FormatStyle::BBO_OnePerLine; + BitwiseOrRule.MinChainLength = 0; + + Style.BreakBinaryOperations.PerOperator = {LogicalRule, BitwiseOrRule}; + + // | operators should break one-per-line. + verifyFormat("int flags = OPTION_VERBOSE_OUTPUT |\n" + " OPTION_RECURSIVE_SCAN |\n" + " OPTION_FORCE_OVERWRITE;", + Style); + + // && still works in multi-group configuration. + verifyFormat("bool valid = isConnectionReady() &&\n" + " isSessionNotExpired() &&\n" + " hasRequiredPermission();", + Style); + + // + stays with default (Never) even with multi-group. + verifyFormat("int total = unitBasePrice + shippingCostPerItem +\n" + " applicableTaxAmount + handlingFeePerUnit;", + Style); + + // | OnePerLine with << sub-expressions: << stays grouped. + Style.BreakBinaryOperations.PerOperator = {BitwiseOrRule}; + verifyFormat("std::uint32_t a = byte_buffer[0] |\n" + " byte_buffer[1] << 8 |\n" + " byte_buffer[2] << 16 |\n" + " byte_buffer[3] << 24;", + Style); + + // >> (stream extraction) OnePerLine: clang-format splits >> into two > + // tokens, but per-operator rules for >> must still work. + FormatStyle::BinaryOperationBreakRule ShiftRightRule; + ShiftRightRule.Operators = {tok::greatergreater}; + ShiftRightRule.Style = FormatStyle::BBO_OnePerLine; + ShiftRightRule.MinChainLength = 0; + + Style.BreakBinaryOperations.PerOperator = {ShiftRightRule}; + verifyFormat("in >>\n" + " packet_id >>\n" + " packet_version >>\n" + " packet_number >>\n" + " packet_scale;", + Style); +} + +TEST_F(FormatTest, BreakBinaryOperationsMinChainLength) { + auto Style = getLLVMStyleWithColumns(60); + + // MinChainLength = 3: chains shorter than 3 don't force breaks. + FormatStyle::BinaryOperationBreakRule LogicalRule; + LogicalRule.Operators = {tok::ampamp, tok::pipepipe}; + LogicalRule.Style = FormatStyle::BBO_OnePerLine; + LogicalRule.MinChainLength = 3; + + Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never; + Style.BreakBinaryOperations.PerOperator = {LogicalRule}; + + // Chain of 2 — below MinChainLength, no forced one-per-line. + verifyFormat("bool ok =\n" + " isConnectionReady(cfg) && isSessionNotExpired(cfg);", + Style); + + // Chain of 3 — meets MinChainLength, one-per-line. + verifyFormat("bool ok = isConnectionReady(cfg) &&\n" + " isSessionNotExpired(cfg) &&\n" + " hasRequiredPermission(cfg);", + Style); + + // Chain of 4 — above MinChainLength, one-per-line. + verifyFormat("bool ok = isConnectionReady(cfg) &&\n" + " isSessionNotExpired(cfg) &&\n" + " hasRequiredPermission(cfg) &&\n" + " isFeatureEnabled(cfg);", + Style); +} + TEST_F(FormatTest, RemoveEmptyLinesInUnwrappedLines) { auto Style = getLLVMStyle(); Style.RemoveEmptyLinesInUnwrappedLines = true; _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
