[clang] [clang-format] Add per-operator granularity for BreakBinaryOperations (PR #181051)

Sergey Subbotin via cfe-commits cfe-commits at lists.llvm.org
Sun Feb 15 13:12:14 PST 2026


https://github.com/ssubbotin updated https://github.com/llvm/llvm-project/pull/181051

>From 83fea1c17681c53aaf1ac946fdc9fa702b90c840 Mon Sep 17 00:00:00 2001
From: Sergey Subbotin <ssubbotin at gmail.com>
Date: Thu, 12 Feb 2026 00:51:35 +0100
Subject: [PATCH 1/2] [clang-format] Add per-operator granularity for
 BreakBinaryOperations

Extend BreakBinaryOperations to accept a structured YAML configuration
with per-operator break rules and minimum chain length gating. The new
PerOperator field allows specifying different break styles (Never,
OnePerLine, RespectPrecedence) for specific operator groups, and
MinChainLength gates breaking to chains of N or more operators.

The simple scalar form remains fully backward-compatible.
---
 clang/docs/ClangFormatStyleOptions.rst     | 72 ++++++++++------
 clang/docs/ReleaseNotes.rst                |  4 +-
 clang/include/clang/Format/Format.h        | 68 +++++++++++++++-
 clang/lib/Format/ContinuationIndenter.cpp  | 46 +++++++++--
 clang/lib/Format/Format.cpp                | 35 +++++++-
 clang/lib/Format/TokenAnnotator.cpp        | 22 +++--
 clang/unittests/Format/ConfigParseTest.cpp | 38 +++++++--
 clang/unittests/Format/FormatTest.cpp      | 95 ++++++++++++++++++++--
 8 files changed, 331 insertions(+), 49 deletions(-)

diff --git a/clang/docs/ClangFormatStyleOptions.rst b/clang/docs/ClangFormatStyleOptions.rst
index 378a0c591d747..100559519c1e6 100644
--- a/clang/docs/ClangFormatStyleOptions.rst
+++ b/clang/docs/ClangFormatStyleOptions.rst
@@ -3613,42 +3613,66 @@ 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;
 
-       aaa +
-       bbbb * ccccc -
-       ddddd +
-       eeeeeeeeeeeeeeee;
 
+  * ``std::vector<BinaryOperationBreakRule> PerOperator`` Per-operator override rules.
 
 
 .. _BreakConstructorInitializers:
diff --git a/clang/docs/ReleaseNotes.rst b/clang/docs/ReleaseNotes.rst
index a1bb1bd2467b7..664731ad3f20e 100644
--- a/clang/docs/ReleaseNotes.rst
+++ b/clang/docs/ReleaseNotes.rst
@@ -360,8 +360,10 @@ AST Matchers
 
 clang-format
 ------------
-- Add ``ObjCSpaceAfterMethodDeclarationPrefix`` option to control space between the 
+- Add ``ObjCSpaceAfterMethodDeclarationPrefix`` option to control space between the
   '-'/'+' and the return type in Objective-C method declarations
+- Extend ``BreakBinaryOperations`` to accept a structured configuration with
+  per-operator break rules and minimum chain length gating via ``PerOperator``.
 
 libclang
 --------
diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 57ae14304b969..3485fc6ec4028 100644
--- a/clang/include/clang/Format/Format.h
+++ b/clang/include/clang/Format/Format.h
@@ -2450,9 +2450,75 @@ struct FormatStyle {
     BBO_RespectPrecedence
   };
 
+  /// A rule that specifies how to break a specific set of binary operators.
+  /// \version 23
+  struct BinaryOperationBreakRule {
+    /// The list of operator tokens this rule applies to,
+    /// e.g. ``["&&", "||"]``.
+    std::vector<std::string> Operators;
+    /// The break style for these operators (defaults to ``OnePerLine``).
+    BreakBinaryOperationsStyle Style;
+    /// Minimum number of chained operators before the rule triggers.
+    /// ``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(StringRef Op) const {
+      for (const auto &Rule : PerOperator) {
+        for (const auto &O : Rule.Operators)
+          if (O == Op)
+            return &Rule;
+      }
+      return nullptr;
+    }
+    BreakBinaryOperationsStyle getStyleForOperator(StringRef Op) const {
+      if (const auto *Rule = findRuleForOperator(Op))
+        return Rule->Style;
+      return Default;
+    }
+    unsigned getMinChainLengthForOperator(StringRef Op) const {
+      if (const auto *Rule = findRuleForOperator(Op))
+        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 1272bb72d423f..c239f8a9e35c0 100644
--- a/clang/lib/Format/ContinuationIndenter.cpp
+++ b/clang/lib/Format/ContinuationIndenter.cpp
@@ -144,14 +144,50 @@ static bool startsNextOperand(const FormatToken &Current) {
   return isAlignableBinaryOperator(Previous) && !Current.isTrailingComment();
 }
 
+// Returns the number of operators in the chain containing \c Op.
+static unsigned getChainLength(const FormatToken &Op) {
+  const FormatToken *Last = &Op;
+  while (Last->NextOperator)
+    Last = Last->NextOperator;
+  return Last->OperatorIndex + 1;
+}
+
 // 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.
+  auto EffStyle =
+      Style.BreakBinaryOperations.getStyleForOperator(OpToken->TokenText);
+  if (EffStyle == FormatStyle::BBO_Never)
+    return false;
+
+  // Check MinChainLength: if the chain is too short, don't force a break.
+  unsigned MinChain = Style.BreakBinaryOperations.getMinChainLengthForOperator(
+      OpToken->TokenText);
+  if (MinChain > 0 && getChainLength(*OpToken) < MinChain)
+    return false;
+
+  return true;
 }
 
 static bool opensProtoMessageField(const FormatToken &LessTok,
diff --git a/clang/lib/Format/Format.cpp b/clang/lib/Format/Format.cpp
index 1e68de531791f..6eeecb2a32413 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -31,6 +31,7 @@
 using clang::format::FormatStyle;
 
 LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::RawStringFormat)
+LLVM_YAML_IS_SEQUENCE_VECTOR(FormatStyle::BinaryOperationBreakRule)
 
 enum BracketAlignmentStyle : int8_t {
   BAS_Align,
@@ -275,6 +276,38 @@ struct ScalarEnumerationTraits<FormatStyle::BreakBinaryOperationsStyle> {
   }
 };
 
+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
@@ -1724,7 +1757,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 9e54c3f253abb..eb6bb2ba5e429 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -3276,14 +3276,22 @@ 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 different precedence level on the same line
-        CurrentPrecedence = prec::Additive;
+        // Check whether this operator's effective style is OnePerLine.
+        // If so, override precedence to avoid adding fake parenthesis which
+        // could group operations of a different precedence level on the same
+        // line.
+        auto EffStyle = Style.BreakBinaryOperations.Default;
+        if (Current) {
+          if (const auto *Rule =
+                  Style.BreakBinaryOperations.findRuleForOperator(
+                      Current->TokenText)) {
+            EffStyle = Rule->Style;
+          }
+        }
+        if (EffStyle == 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 0a116b770f52a..bef3b9002590b 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -453,13 +453,41 @@ TEST(ConfigParseTest, ParsesConfiguration) {
   CHECK_PARSE("BreakBeforeBinaryOperators: true", BreakBeforeBinaryOperators,
               FormatStyle::BOS_All);
 
-  Style.BreakBinaryOperations = FormatStyle::BBO_Never;
+  Style.BreakBinaryOperations = {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 = {FormatStyle::BBO_Never, {}};
+  CHECK_PARSE("BreakBinaryOperations:\n"
+              "  Default: OnePerLine",
+              BreakBinaryOperations,
+              FormatStyle::BreakBinaryOperationsOptions(
+                  {FormatStyle::BBO_OnePerLine, {}}));
+
+  Style.BreakBinaryOperations = {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<std::string> ExpectedOps = {"&&", "||"};
+  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);
 
   Style.BreakConstructorInitializers = FormatStyle::BCIS_BeforeColon;
   CHECK_PARSE("BreakConstructorInitializers: BeforeComma",
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index 33836e28289b4..f0eb12177d07b 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -28355,7 +28355,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"
@@ -28394,7 +28396,7 @@ TEST_F(FormatTest, BreakBinaryOperations) {
                "    longOperand_3_;",
                Style);
 
-  Style.BreakBinaryOperations = FormatStyle::BBO_OnePerLine;
+  Style.BreakBinaryOperations = {FormatStyle::BBO_OnePerLine, {}};
 
   // Logical operations
   verifyFormat("if (condition1 && condition2) {\n"
@@ -28479,7 +28481,7 @@ TEST_F(FormatTest, BreakBinaryOperations) {
                "    longOperand_3_;",
                Style);
 
-  Style.BreakBinaryOperations = FormatStyle::BBO_RespectPrecedence;
+  Style.BreakBinaryOperations = {FormatStyle::BBO_RespectPrecedence, {}};
   verifyFormat("result = op1 + op2 * op3 - op4;", Style);
 
   verifyFormat("result = operand1 +\n"
@@ -28511,7 +28513,7 @@ TEST_F(FormatTest, BreakBinaryOperations) {
                "    longOperand_3_;",
                Style);
 
-  Style.BreakBinaryOperations = FormatStyle::BBO_OnePerLine;
+  Style.BreakBinaryOperations = {FormatStyle::BBO_OnePerLine, {}};
   Style.BreakBeforeBinaryOperators = FormatStyle::BOS_NonAssignment;
 
   // Logical operations
@@ -28592,7 +28594,7 @@ TEST_F(FormatTest, BreakBinaryOperations) {
                "    >> longOperand_3_;",
                Style);
 
-  Style.BreakBinaryOperations = FormatStyle::BBO_RespectPrecedence;
+  Style.BreakBinaryOperations = {FormatStyle::BBO_RespectPrecedence, {}};
   verifyFormat("result = op1 + op2 * op3 - op4;", Style);
 
   verifyFormat("result = operand1\n"
@@ -28625,6 +28627,89 @@ 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 = {"&&", "||"};
+  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 x = loooooooooooooongcondition1 &&\n"
+               "         loooooooooooooongcondition2 &&\n"
+               "         loooooooooooooongcondition3;",
+               Style);
+
+  // Arithmetic operators stay with default (Never) — no forced break.
+  verifyFormat("int x = loooooooooooooongop1 + looooooooongop2 +\n"
+               "        loooooooooooooooooooooongop3;",
+               Style);
+
+  // Short logical chain that fits on one line stays on one line.
+  verifyFormat("bool x = a && b && c;", Style);
+
+  // Multiple PerOperator groups: && and || plus | operators.
+  FormatStyle::BinaryOperationBreakRule BitwiseOrRule;
+  BitwiseOrRule.Operators = {"|"};
+  BitwiseOrRule.Style = FormatStyle::BBO_OnePerLine;
+  BitwiseOrRule.MinChainLength = 0;
+
+  Style.BreakBinaryOperations.PerOperator = {LogicalRule, BitwiseOrRule};
+
+  // | operators should break one-per-line.
+  verifyFormat("int x = loooooooooooooooooongval1 |\n"
+               "        loooooooooooooooooongval2 |\n"
+               "        loooooooooooooooooongval3;",
+               Style);
+
+  // && still works in multi-group configuration.
+  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
+               "         loooooooooooooongcondition2 &&\n"
+               "         loooooooooooooongcondition3;",
+               Style);
+
+  // + operators stay with default (Never) even with multi-group.
+  verifyFormat("int x = loooooooooooooongop1 + looooooooongop2 +\n"
+               "        loooooooooooooooooooooongop3;",
+               Style);
+}
+
+TEST_F(FormatTest, BreakBinaryOperationsMinChainLength) {
+  auto Style = getLLVMStyleWithColumns(60);
+
+  // MinChainLength = 3: chains shorter than 3 don't force breaks.
+  FormatStyle::BinaryOperationBreakRule LogicalRule;
+  LogicalRule.Operators = {"&&", "||"};
+  LogicalRule.Style = FormatStyle::BBO_OnePerLine;
+  LogicalRule.MinChainLength = 3;
+
+  Style.BreakBinaryOperations.Default = FormatStyle::BBO_Never;
+  Style.BreakBinaryOperations.PerOperator = {LogicalRule};
+
+  // Chain of 2 — should NOT force one-per-line (below MinChainLength).
+  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
+               "         loooooooooooooongcondition2;",
+               Style);
+
+  // Chain of 3 — should force one-per-line.
+  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
+               "         loooooooooooooongcondition2 &&\n"
+               "         loooooooooooooongcondition3;",
+               Style);
+
+  // Chain of 4 — should force one-per-line.
+  verifyFormat("bool x = looooooooooongcondition1 &&\n"
+               "         looooooooooongcondition2 &&\n"
+               "         looooooooooongcondition3 &&\n"
+               "         looooooooooongcondition4;",
+               Style);
+}
+
 TEST_F(FormatTest, RemoveEmptyLinesInUnwrappedLines) {
   auto Style = getLLVMStyle();
   Style.RemoveEmptyLinesInUnwrappedLines = true;

>From 258f4f32801bb796ce84296ea51213c51f119cfa Mon Sep 17 00:00:00 2001
From: Sergey Subbotin <ssubbotin at gmail.com>
Date: Sun, 15 Feb 2026 22:11:47 +0100
Subject: [PATCH 2/2] [clang-format] Address review: use tok::TokenKind for
 operator matching

- Store tok::TokenKind instead of strings in BinaryOperationBreakRule
  to handle alternative operator spellings (e.g. 'and' vs '&&')
- Use llvm::find instead of manual loop in findRuleForOperator
- Rename EffStyle to OperatorBreakStyle and make it const
- Simplify MinChainLength check
- Use more realistic variable names in tests
---
 clang/include/clang/Format/Format.h        | 24 +++++----
 clang/lib/Format/ContinuationIndenter.cpp  | 16 +++---
 clang/lib/Format/Format.cpp                | 25 +++++++++
 clang/lib/Format/TokenAnnotator.cpp        | 14 ++---
 clang/unittests/Format/ConfigParseTest.cpp |  2 +-
 clang/unittests/Format/FormatTest.cpp      | 62 +++++++++++-----------
 6 files changed, 82 insertions(+), 61 deletions(-)

diff --git a/clang/include/clang/Format/Format.h b/clang/include/clang/Format/Format.h
index 3485fc6ec4028..52042dd6609b2 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"
@@ -2453,9 +2454,10 @@ struct FormatStyle {
   /// A rule that specifies how to break a specific set of binary operators.
   /// \version 23
   struct BinaryOperationBreakRule {
-    /// The list of operator tokens this rule applies to,
-    /// e.g. ``["&&", "||"]``.
-    std::vector<std::string> Operators;
+    /// The list of operator token kinds this rule applies to.
+    /// Stored as ``tok::TokenKind`` so that alternative spellings
+    /// (e.g. ``and`` vs ``&&``) are handled automatically.
+    std::vector<tok::TokenKind> Operators;
     /// The break style for these operators (defaults to ``OnePerLine``).
     BreakBinaryOperationsStyle Style;
     /// Minimum number of chained operators before the rule triggers.
@@ -2490,21 +2492,21 @@ struct FormatStyle {
     BreakBinaryOperationsStyle Default;
     /// Per-operator override rules.
     std::vector<BinaryOperationBreakRule> PerOperator;
-    const BinaryOperationBreakRule *findRuleForOperator(StringRef Op) const {
+    const BinaryOperationBreakRule *
+    findRuleForOperator(tok::TokenKind Kind) const {
       for (const auto &Rule : PerOperator) {
-        for (const auto &O : Rule.Operators)
-          if (O == Op)
-            return &Rule;
+        if (llvm::find(Rule.Operators, Kind) != Rule.Operators.end())
+          return &Rule;
       }
       return nullptr;
     }
-    BreakBinaryOperationsStyle getStyleForOperator(StringRef Op) const {
-      if (const auto *Rule = findRuleForOperator(Op))
+    BreakBinaryOperationsStyle getStyleForOperator(tok::TokenKind Kind) const {
+      if (const auto *Rule = findRuleForOperator(Kind))
         return Rule->Style;
       return Default;
     }
-    unsigned getMinChainLengthForOperator(StringRef Op) const {
-      if (const auto *Rule = findRuleForOperator(Op))
+    unsigned getMinChainLengthForOperator(tok::TokenKind Kind) const {
+      if (const auto *Rule = findRuleForOperator(Kind))
         return Rule->MinChainLength;
       return 0;
     }
diff --git a/clang/lib/Format/ContinuationIndenter.cpp b/clang/lib/Format/ContinuationIndenter.cpp
index c239f8a9e35c0..7fb7b5d0724ae 100644
--- a/clang/lib/Format/ContinuationIndenter.cpp
+++ b/clang/lib/Format/ContinuationIndenter.cpp
@@ -176,18 +176,16 @@ static bool mustBreakBinaryOperation(const FormatToken &Current,
   }
 
   // Look up per-operator rule or fall back to Default.
-  auto EffStyle =
-      Style.BreakBinaryOperations.getStyleForOperator(OpToken->TokenText);
-  if (EffStyle == FormatStyle::BBO_Never)
+  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.
-  unsigned MinChain = Style.BreakBinaryOperations.getMinChainLengthForOperator(
-      OpToken->TokenText);
-  if (MinChain > 0 && getChainLength(*OpToken) < MinChain)
-    return false;
-
-  return true;
+  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 6eeecb2a32413..f72ff0bcb01b3 100644
--- a/clang/lib/Format/Format.cpp
+++ b/clang/lib/Format/Format.cpp
@@ -32,6 +32,7 @@ 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,
@@ -276,6 +277,30 @@ 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);
diff --git a/clang/lib/Format/TokenAnnotator.cpp b/clang/lib/Format/TokenAnnotator.cpp
index eb6bb2ba5e429..ca3bdcd21a63d 100644
--- a/clang/lib/Format/TokenAnnotator.cpp
+++ b/clang/lib/Format/TokenAnnotator.cpp
@@ -3282,15 +3282,11 @@ class ExpressionParser {
         // If so, override precedence to avoid adding fake parenthesis which
         // could group operations of a different precedence level on the same
         // line.
-        auto EffStyle = Style.BreakBinaryOperations.Default;
-        if (Current) {
-          if (const auto *Rule =
-                  Style.BreakBinaryOperations.findRuleForOperator(
-                      Current->TokenText)) {
-            EffStyle = Rule->Style;
-          }
-        }
-        if (EffStyle == FormatStyle::BBO_OnePerLine)
+        const auto OperatorBreakStyle =
+            Current ? Style.BreakBinaryOperations.getStyleForOperator(
+                          Current->Tok.getKind())
+                    : Style.BreakBinaryOperations.Default;
+        if (OperatorBreakStyle == FormatStyle::BBO_OnePerLine)
           CurrentPrecedence = prec::Additive;
       }
 
diff --git a/clang/unittests/Format/ConfigParseTest.cpp b/clang/unittests/Format/ConfigParseTest.cpp
index bef3b9002590b..1fff3a83be63e 100644
--- a/clang/unittests/Format/ConfigParseTest.cpp
+++ b/clang/unittests/Format/ConfigParseTest.cpp
@@ -483,7 +483,7 @@ TEST(ConfigParseTest, ParsesConfiguration) {
                    .value());
   EXPECT_EQ(Style.BreakBinaryOperations.Default, FormatStyle::BBO_Never);
   ASSERT_EQ(Style.BreakBinaryOperations.PerOperator.size(), 1u);
-  std::vector<std::string> ExpectedOps = {"&&", "||"};
+  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);
diff --git a/clang/unittests/Format/FormatTest.cpp b/clang/unittests/Format/FormatTest.cpp
index f0eb12177d07b..1e1cd1d5f8867 100644
--- a/clang/unittests/Format/FormatTest.cpp
+++ b/clang/unittests/Format/FormatTest.cpp
@@ -28632,7 +28632,7 @@ TEST_F(FormatTest, BreakBinaryOperationsPerOperator) {
 
   // Per-operator override: && and || are OnePerLine, rest is Never (default).
   FormatStyle::BinaryOperationBreakRule LogicalRule;
-  LogicalRule.Operators = {"&&", "||"};
+  LogicalRule.Operators = {tok::ampamp, tok::pipepipe};
   LogicalRule.Style = FormatStyle::BBO_OnePerLine;
   LogicalRule.MinChainLength = 0;
 
@@ -28640,42 +28640,42 @@ TEST_F(FormatTest, BreakBinaryOperationsPerOperator) {
   Style.BreakBinaryOperations.PerOperator = {LogicalRule};
 
   // Logical operators break one-per-line when line is too long.
-  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
-               "         loooooooooooooongcondition2 &&\n"
-               "         loooooooooooooongcondition3;",
+  verifyFormat("bool valid = isConnectionReady() &&\n"
+               "             isSessionNotExpired() &&\n"
+               "             hasRequiredPermission();",
                Style);
 
-  // Arithmetic operators stay with default (Never) — no forced break.
-  verifyFormat("int x = loooooooooooooongop1 + looooooooongop2 +\n"
-               "        loooooooooooooooooooooongop3;",
+  // Arithmetic operators stay with default (Never).
+  verifyFormat("int total = unitBasePrice + shippingCostPerItem +\n"
+               "            applicableTaxAmount + handlingFeePerUnit;",
                Style);
 
-  // Short logical chain that fits on one line stays on one line.
+  // 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 = {"|"};
+  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 x = loooooooooooooooooongval1 |\n"
-               "        loooooooooooooooooongval2 |\n"
-               "        loooooooooooooooooongval3;",
+  verifyFormat("int flags = OPTION_VERBOSE_OUTPUT |\n"
+               "            OPTION_RECURSIVE_SCAN |\n"
+               "            OPTION_FORCE_OVERWRITE;",
                Style);
 
   // && still works in multi-group configuration.
-  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
-               "         loooooooooooooongcondition2 &&\n"
-               "         loooooooooooooongcondition3;",
+  verifyFormat("bool valid = isConnectionReady() &&\n"
+               "             isSessionNotExpired() &&\n"
+               "             hasRequiredPermission();",
                Style);
 
-  // + operators stay with default (Never) even with multi-group.
-  verifyFormat("int x = loooooooooooooongop1 + looooooooongop2 +\n"
-               "        loooooooooooooooooooooongop3;",
+  // + stays with default (Never) even with multi-group.
+  verifyFormat("int total = unitBasePrice + shippingCostPerItem +\n"
+               "            applicableTaxAmount + handlingFeePerUnit;",
                Style);
 }
 
@@ -28684,29 +28684,29 @@ TEST_F(FormatTest, BreakBinaryOperationsMinChainLength) {
 
   // MinChainLength = 3: chains shorter than 3 don't force breaks.
   FormatStyle::BinaryOperationBreakRule LogicalRule;
-  LogicalRule.Operators = {"&&", "||"};
+  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 — should NOT force one-per-line (below MinChainLength).
-  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
-               "         loooooooooooooongcondition2;",
+  // Chain of 2 — below MinChainLength, no forced one-per-line.
+  verifyFormat("bool ok =\n"
+               "    isConnectionReady(cfg) && isSessionNotExpired(cfg);",
                Style);
 
-  // Chain of 3 — should force one-per-line.
-  verifyFormat("bool x = loooooooooooooongcondition1 &&\n"
-               "         loooooooooooooongcondition2 &&\n"
-               "         loooooooooooooongcondition3;",
+  // Chain of 3 — meets MinChainLength, one-per-line.
+  verifyFormat("bool ok = isConnectionReady(cfg) &&\n"
+               "          isSessionNotExpired(cfg) &&\n"
+               "          hasRequiredPermission(cfg);",
                Style);
 
-  // Chain of 4 — should force one-per-line.
-  verifyFormat("bool x = looooooooooongcondition1 &&\n"
-               "         looooooooooongcondition2 &&\n"
-               "         looooooooooongcondition3 &&\n"
-               "         looooooooooongcondition4;",
+  // Chain of 4 — above MinChainLength, one-per-line.
+  verifyFormat("bool ok = isConnectionReady(cfg) &&\n"
+               "          isSessionNotExpired(cfg) &&\n"
+               "          hasRequiredPermission(cfg) &&\n"
+               "          isFeatureEnabled(cfg);",
                Style);
 }
 



More information about the cfe-commits mailing list