Skip to content

avoid_complex_conditions

v1.0.0WarningConfigurableCode Quality

This rule flags a boolean expression combining more &&/|| operands than the configured budget.

a && b && !c || d forces the reader to hold four facts and two precedence rules at once, and it is where an && that should have been || hides longest. Naming the parts makes the condition readable at a glance and debuggable one piece at a time.

This rule is in the pedantic preset. The default budget is max_operands: 3.

Four operands in one expression:

class Account {
const Account(this.isActive, this.hasPaidBalance, this.isSuspended, this.age);
final bool isActive;
final bool hasPaidBalance;
final bool isSuspended;
final int age;
}
bool canCheckOut(Account account) {
return account.isActive &&
account.hasPaidBalance &&
!account.isSuspended &&
account.age >= 18;
}

Name the two halves. The condition now reads as one sentence, and either half can be tested on its own:

class Account {
const Account(this.isActive, this.hasPaidBalance, this.isSuspended, this.age);
final bool isActive;
final bool hasPaidBalance;
final bool isSuspended;
final int age;
}
bool canCheckOut(Account account) {
final isInGoodStanding = account.isActive && account.hasPaidBalance;
final isPermitted = !account.isSuspended && account.age >= 18;
return isInGoodStanding && isPermitted;
}

The diagnostic lands on the root of the chain, so a long condition reports once rather than once per operator. Parentheses do not break a chain either:

// One diagnostic, five operands — not three overlapping ones.
bool ready(bool a, bool b, bool c, bool d, bool e) =>
(a && b) && (c || d) && e;

Each !x counts as one operand, not two — only && and || add to the count.

Any boolean expression is measured, wherever it appears — a return, a variable initialiser, an argument:

class Filter {
Filter(this.a, this.b, this.c, this.d);
final bool a;
final bool b;
final bool c;
final bool d;
// Reported: four operands in a field initialiser.
late final bool matches = a && b && c && d;
}

It is one && per field by construction, and splitting it would scatter an equality check that reads as a unit. This accounted for most of the hits when the rule was first run against a production codebase:

class Booking {
const Booking(this.id, this.court, this.start, this.end, this.notes);
final String id;
final int court;
final DateTime start;
final DateTime end;
final String? notes;
// Not reported, however many fields it grows to.
@override
bool operator ==(Object other) =>
other is Booking &&
other.id == id &&
other.court == court &&
other.start == start &&
other.end == end &&
other.notes == notes;
@override
int get hashCode => Object.hash(id, court, start, end, notes);
}

The exemption covers anything inside a member named ==, not just its return expression.

Three operands is one && chain a reader can still hold. Drop it to two if you want every compound condition named:

many_lints.yaml
rules:
avoid_complex_conditions:
max_operands: 2

This rule is in the pedantic preset, so it is enabled by preset: pedantic or by name:

many_lints.yaml
rules:
avoid_complex_conditions:
enabled: true
analysis_options.yaml
many_lints:
rules:
avoid_complex_conditions:
max_operands: 2
Option Type Default Description
max_operands int 3 How many &&/`

To disable this rule:

many_lints.yaml
rules:
avoid_complex_conditions: false

To keep the rule on but skip certain paths, use per-rule exclude.