Skip to content

avoid_collapsible_if

v0.8.0 Warning Fix Control Flow

Flags an if whose body is nothing but another if, where neither has an else. Two nested conditions with no else are a conjunction written across two blocks.

class User {
bool isActive = true;
}
void notify(User? user) {
if (user != null) {
if (user.isActive) {
sendNotification(user);
}
}
}
void sendNotification(User user) {}
class User {
bool isActive = true;
}
void notify(User? user) {
if (user != null && user.isActive) {
sendNotification(user);
}
}
void sendNotification(User user) {}

The rule matches the inner if whether it is the sole statement of a block or written bare:

void log(String? message, bool verbose) {
if (verbose)
if (message != null) print(message);
}

That collapses the same way:

void log(String? message, bool verbose) {
if (verbose && message != null) print(message);
}

The quick fix parenthesises an operand when merging would otherwise change precedence, so the meaning is preserved:

void handle(bool retryable, int status, bool offline) {
// Don't
if (retryable) {
if (status >= 500 || offline) {
scheduleRetry();
}
}
// Do — the fix writes this, not `retryable && status >= 500 || offline`
if (retryable && (status >= 500 || offline)) {
scheduleRetry();
}
}
void scheduleRetry() {}

The rule stays silent whenever the nesting could carry meaning:

  • An else on either level.
  • Any other statement in the outer block, before or after the inner if.
  • A pattern if (x case P) on either level, which cannot be joined with &&.

Three collapsible levels produce two reports — one per mergeable pair — and merging them all gives a single && chain.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated. Add it to preset: core with avoid_collapsible_if: true.

To turn it off:

many_lints.yaml
rules:
avoid_collapsible_if: false

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