Skip to content

avoid_cascade_after_if_null

v0.3.0 Warning Fix Control Flow

Flags a cascade (..) whose target is an if-null (??) expression that is not parenthesised.

?? binds tighter than .., so a ?? B()..m() parses as (a ?? B())..m(). The cascade runs against whichever side ?? produced — including a itself when a is non-null. That is the opposite of how the line reads.

See also: Cascade notation

The intent here is “reuse the spare kettle if there is one, otherwise build one and boil it”. What the code does is boil whichever kettle ?? returned — including the caller’s spare:

class Kettle {
int litres = 1;
void boil() {}
}
Kettle heat(Kettle? spare) {
return spare ?? Kettle()..boil();
}

Two readings, two sets of parentheses. Pick the one you meant.

Boil whichever kettle you ended up with:

class Kettle {
int litres = 1;
void boil() {}
}
Kettle heat(Kettle? spare) {
return (spare ?? Kettle())..boil();
}

Boil only the fresh one, and leave the caller’s spare untouched:

class Kettle {
int litres = 1;
void boil() {}
}
Kettle heat(Kettle? spare) {
return spare ?? (Kettle()..boil());
}

The two forms are not interchangeable — they mutate different objects:

void demo() {
StringBuffer? spare = StringBuffer('spare');
final a = spare ?? StringBuffer('fresh')..write('!');
// The cascade hit `spare`: it now reads 'spare!', the fresh buffer was
// built, never written to, and thrown away. `a` is `spare`.
final b = spare ?? (StringBuffer('fresh')..write('!'));
// `spare` is untouched, and `b` is `spare` — the fresh buffer was never
// built at all.
}

Every section of the cascade lands on the same mis-chosen target:

class Kettle {
int litres = 1;
void boil() {}
}
Kettle heat(Kettle? spare) {
return (spare ?? Kettle())
..litres = 5
..boil();
}

Only a bare ?? target is reported. A cascade with no ?? in sight (Kettle()..boil()) and one already parenthesised on either side are both left alone — the parentheses are exactly what the rule is asking for, so once they are there the report goes away.

The quick fix wraps the if-null expression, producing (a ?? B())..m(). That is the more common intent, but it is not always the one you want; check the result against the “Do” forms above before accepting it.

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

To turn it off:

many_lints.yaml
rules:
avoid_cascade_after_if_null: false

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