cascade-invocations

Same as cascade_invocations in Dart lints

Path
lint/complexity/cascade-invocations
Default
Off, opt in
Origin
Dart lints
Rule
199 of 512

Flags consecutive statements that invoke members on the same variable.

final list = <int>[];   // bad
list.add(1);
list.add(2);

final list = <int>[]    // good
  ..add(1)
  ..add(2);

Within one block (or switch case body) the rule follows the official lint's cascade model. A statement that calls an instance method on, reads a property of, sets a property of, or cascades on a simple identifier v can join the preceding statement when that one targeted the same v and can receive: a declaration of the local v (not const), an assignment to the local v (not ??=), a property set on a local v (not ??=), or a method call, property read, or cascade on v. Assignments to fields and top-level variables never receive (the analyzer sees their setter, not the variable), nor do property sets through them. Null-aware accesses (v?.m()) and calls of static methods never join. Method calls require a resolved non-static method: dynamic invocations and function call invocations do not qualify. Parenthesized receivers are not simple identifiers, while property assignments still accept dynamic locals. Receiver types come from shared resolution, including imported top-level getters/variables and implicit method calls used as cascade initializers; genuinely unresolved receivers stay silent rather than guessing a method.

A statement whose member names, arguments, or right-hand side depend on the receiver breaks the chain, since a cascade would evaluate them before the earlier member access: a reference to the same local declaration (not a shadowing closure parameter), and for a field or top-level-variable receiver also a closure or a reference to any member declared in the same class as the field (respectively any top-level declaration of the same library). One diagnostic covers each run of joinable statements, from the first statement that could be cascaded to the last one. Where falcon cannot resolve a referenced member's declaring class (for example through an external type), it assumes a dependency and stays silent.

Invalid

example.dartdart
class Point {
  int x = 0;
  int y = 0;
  void reset() {}
  void move(int dx) {}
}

class Holder {
  final List<int> items = [];

  void fill() {
    items.add(1);
    items.add(2);
  }
}

void declared() {
  final list = <int>[];
  list.add(1);
  list.add(2);
}

void parameter(Point p) {
  p.reset();
  p.move(1);
}

void setters(Point p) {
  p.x = 1;
  p.y = 2;
}

void assigned(Point p) {
  p = Point();
  p.reset();
}

// Source-defined contract keeps cascade resolution independent of SDK indexes.
class Buffer {
  void write(Object value) {}
}

void cascades(Buffer buffer) {
  buffer..write('a')..write('b');
  buffer.write('c');
}

void inSwitch(int value, List<int> list) {
  switch (value) {
    case 1:
      list.clear();
      list.add(value);
  }
}

void broken(List<int> list, List<int> other) {
  list.add(1);
  list.add(2);
  other.add(3);
  list.add(4);
}

// Members of another class than the receiver field's are no dependency.
class Out {
  void write(Object value) {}
}

class Writer {
  final Out buffer = Out();
}

class PrettyWriter extends Writer {
  final String indent = '  ';

  void write() {
    buffer.write('a');
    buffer.write(indent);
  }
}

// A field receiver may take `this` or other objects' members as arguments.
class Field {
  final Point point = Point();

  void run(Point other) {
    point.reset();
    point.move(other.x);
    print(this);
  }
}

// Assignment eligibility uses the receiver variable, not the setter element.
void dynamicSetters(dynamic receiver) {
  receiver.x = 1;
  receiver.y = 2;
}

void dynamicObjectMethods(dynamic receiver) {
  receiver.toString();
  receiver.toString();
}

class ScopedReceiver {
  void initialize(void Function(ScopedReceiver) callback) {}
  void reset() {}
}

// The callback parameter is not a dependency on the outer receiver.
void shadowedCallback() {
  final receiver = ScopedReceiver();
  receiver.initialize((receiver) {
    receiver.reset();
  });
  receiver.reset();
}
Avoid repeating the receiver.
12 items.add(1);
13 items.add(2);
Avoid repeating the receiver.
18 final list = <int>[];
19 list.add(1);
Avoid repeating the receiver.
24 p.reset();
25 p.move(1);
Avoid repeating the receiver.
29 p.x = 1;
30 p.y = 2;
Avoid repeating the receiver.
34 p = Point();
35 p.reset();
Avoid repeating the receiver.
44 buffer..write('a')..write('b');
45 buffer.write('c');
Avoid repeating the receiver.
51 list.clear();
52 list.add(value);
Avoid repeating the receiver.
57 list.add(1);
58 list.add(2);
Avoid repeating the receiver.
76 buffer.write('a');
77 buffer.write(indent);
Avoid repeating the receiver.
86 point.reset();
87 point.move(other.x);
Avoid repeating the receiver.
94 receiver.x = 1;
95 receiver.y = 2;
Avoid repeating the receiver.
99 receiver.toString();
100 receiver.toString();
Avoid repeating the receiver.
110 final receiver = ScopedReceiver();
111 receiver.initialize((receiver) {

Valid

example.dartdart
class Point {
  int x = 0;
  void reset() {}
  static void create() {}
  static void destroy() {}
}

void cascaded() {
  final list = <int>[]
    ..add(1)
    ..add(2);
  print(list);
}

void different(List<int> a, List<int> b) {
  a.add(1);
  b.add(2);
}

void nullAware(List<int>? list) {
  list?.add(1);
  list?.add(2);
}

void staticCalls() {
  Point.create();
  Point.destroy();
}

void dependency(List<int> list) {
  list.add(1);
  list.add(list.length);
}

void awaited() async {
  final values = await Future.value(<int>[]);
  values.add(1);
}

void constDeclaration() {
  const list = <int>[];
  list.length;
}

void ifNull(List<int>? list) {
  list ??= [];
  list.add(1);
}

void single(List<int> list) {
  list.add(1);
}

void separated(List<int> list) {
  list.add(1);
  print('x');
  list.add(2);
}

void chained(Map<String, List<int>> map) {
  map['a']!.add(1);
  map['a']!.add(2);
}

class Listener {
  void addListener(void Function() f) {}
  void removeListener(void Function() f) {}
  void layout() {}
  int width = 0;
}

class Owner<T> {
  final Listener first = Listener();
  final Listener second = Listener();
  final List<T> generic = [];
  void _onChange() {}

  // A tear-off of the field's own class may observe the field.
  void listen() {
    first.addListener(_onChange);
    first.removeListener(_onChange);
  }

  // So may another field of the same class.
  void fields() {
    first.layout();
    first.addListener(second.layout);
  }

  // Property sets through a field never receive a cascade.
  void sets() {
    first.width = 1;
    first.layout();
  }

  Listener third = Listener();

  // Assigning a field writes its setter, a different element than the getter.
  void assigns() {
    third = Listener();
    third.layout();
  }

  // A field typed with the class's type parameters is a fresh member per use.
  void typed(T value) {
    generic.add(value);
    generic.add(value);
  }
}

class Callbacks {
  final void Function() callback = () {};
}

// Calling a function-typed field is no method invocation.
void invokeTwice(Callbacks callbacks) {
  callbacks.callback();
  callbacks.callback();
}

// Dynamic calls have no resolved method element.
void dynamicMethods(dynamic receiver) {
  receiver.first();
  receiver.second();
}

void functionCalls(Function callback) {
  callback.call();
  callback.call();
}

void typedFunctionCalls(void Function() callback) {
  callback.call();
  callback.call();
}

void parenthesizedTargets(Point point) {
  (point).reset();
  (point).reset();
}

class ScopedReceiver {
  void initialize(void Function(ScopedReceiver) callback) {}
  void reset() {}
}

// A captured reference to the outer receiver still breaks the cascade.
void capturedCallback(ScopedReceiver receiver) {
  receiver.initialize((other) {
    receiver.reset();
  });
  receiver.reset();
}

Configure cascade-invocations

Set its severity in falcon.json. See the configuration guide.

falcon.jsonjson
{
  "linter": {
    "rules": {
      "complexity": {
        "cascade-invocations": "error"
      }
    }
  }
}

Or silence one occurrence with a suppression comment; the reason is required.

example.dartdart
// falcon-ignore lint/complexity/cascade-invocations: <reason>
  • cognitive-complexity

    Reports function bodies with excessive nesting-weighted control flow.

  • cyclomatic-complexitycyclomatic_complexity

    Flags a function whose cyclomatic complexity exceeds the configured threshold.

  • join-return-with-assignmentjoin_return_with_assignment

    Flags an assignment immediately followed by return of the same variable.

  • max-lines-for-filemax_lines_for_file

    Flags a file longer than the configured line limit.

  • max-lines-for-functionmax_lines_for_function

    Flags a function or method longer than the configured line limit.

  • max-parameters-for-functionmax_parameters_for_function

    Flags callable declarations and function types with more than the configured number of parameters.

  • max-statements

    Reports function bodies with too many statements.

  • max-switch-casesmax_switch_cases

    Flags a switch statement with more than the configured number of cases.