[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
This commit is contained in:
Sergey Subbotin 2026-02-21 23:08:18 +01:00 committed by GitHub
parent 5e0ef90141
commit 62e55b410c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 473 additions and 52 deletions

View File

@ -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:
aaa +
bbbb * ccccc -
ddddd +
eeeeeeeeeeeeeeee;
* ``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;
* ``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:

View File

@ -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
--------

View File

@ -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 @@ class NestedField(object):
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 OptionsReader:
)
)
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("///"):

View File

@ -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 {

View File

@ -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,

View File

@ -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;

View File

@ -3276,14 +3276,19 @@ public:
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;
// 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 &&

View File

@ -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",

View File

@ -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;