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

Reply via email to