cancel-subscriptions

Same as cancel_subscriptions in Dart lints

Path
lint/correctness/cancel-subscriptions
Default
On (recommended)
Origin
Dart lints
Rule
197 of 512
recommended

Cancel instances of dart:async StreamSubscription.

A subscription that is never cancelled keeps its stream (and everything the listener captures) alive, leaking memory and running callbacks after their owner is gone:

class Watcher {
  StreamSubscription<int>? _sub; // BAD: never cancelled
}

class Watcher {
  StreamSubscription<int>? _sub;
  void dispose() => _sub?.cancel(); // GOOD
}

Fields and local variables whose type provably implements StreamSubscription are reported unless the file (fields) or enclosing function body (locals) holds an official valid-use/escape case. Constructor arguments and assignments into another local do not escape; references to shadowing variables never cancel the original. Unresolved types stay silent.

Invalid

example.dartdart
import 'dart:async';

class Watcher {
  StreamSubscription<int>? _subscription;
  late StreamSubscription<String> other;

  void start(Stream<int> stream) {
    _subscription = stream.listen((_) {});
  }
}

void listenLocally(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  subscription.pause();
}

void typedLocal(Stream<int> stream) {
  StreamSubscription<int> subscription = stream.listen(print);
  subscription.resume();
}

void fromController(StreamController<int> controller) {
  var subscription = controller.stream.listen((_) {});
  print(subscription.isPaused);
}

void inClosure(Stream<int> stream) {
  final run = () {
    final subscription = stream.listen((_) {});
    subscription.pause();
  };
  run();
}

void aliasedDeclaration(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  final alias = subscription;
  alias.cancel();
}
void reassigned(Stream<int> stream, StreamSubscription<int>? holder) {
  final subscription = stream.listen((_) {});
  holder = subscription;
  holder.cancel();
}

class SubscriptionBox {
  final StreamSubscription<int> subscription;
  SubscriptionBox(this.subscription);
}

void constructorArgument(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  SubscriptionBox(subscription);
}

void functionArgument(Stream<int> stream, void Function(StreamSubscription<int>) callback) {
  final subscription = stream.listen((_) {});
  callback(subscription);
}

void assertedAlias(StreamSubscription<int>? source) {
  final subscription = source!;
  subscription.pause();
}

void shadowedClosure(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  final release = (StreamSubscription<int> subscription) => subscription.cancel();
  release(stream.listen((_) {}));
}

void localCascade(Stream<int> stream) {
  final subscription = stream.listen((_) {})..pause();
}

class ThroughThisPause {
  StreamSubscription<int>? subscription;
  void hold() => this.subscription?.pause();
}

class LeakedField {
  StreamSubscription<int>? subscription;
}

class ReleasedField {
  StreamSubscription<int>? subscription;
  void dispose() => subscription?.cancel();
}

class GenericSubscriptionBox<T> {
  GenericSubscriptionBox.named(StreamSubscription<T> subscription);
}

void genericConstructorArgument(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  GenericSubscriptionBox<int>.named(subscription);
}

class FactorySubscriptionBox<T> {
  FactorySubscriptionBox._(StreamSubscription<T> subscription);

  factory FactorySubscriptionBox(Stream<T> stream) {
    final subscription = stream.listen((_) {});
    return FactorySubscriptionBox<T>._(subscription);
  }
}

void dynamicCallback(Stream<int> stream, dynamic callback) {
  final subscription = stream.listen((_) {});
  callback(subscription);
}

void assertedCallback(Stream<int> stream, void Function(StreamSubscription<int>)? callback) {
  final subscription = stream.listen((_) {});
  callback!(subscription);
}

void immediateCallback(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  ((StreamSubscription<int> value) {})(subscription);
}

void functionCallback(Stream<int> stream, Function callback) {
  final subscription = stream.listen((_) {});
  callback(subscription);
}

class CallbackBox {
  CallbackBox(this.callback);
  final void Function(StreamSubscription<int>) callback;

  void pass(Stream<int> stream) {
    final subscription = stream.listen((_) {});
    this.callback(subscription);
  }
}

void getterCallback(Stream<int> stream, CallbackBox box) {
  final subscription = stream.listen((_) {});
  box.callback(subscription);
}

abstract class ChainedSubscription implements StreamSubscription<int> {
  StreamSubscription<int> next();
}

ChainedSubscription createSubscription() => throw StateError('unavailable');

void chainedCascadeRelease() {
  final subscription = createSubscription();
  subscription..next().cancel();
}

void indexedCallback(Stream<int> stream, List<void Function(StreamSubscription<int>)> callbacks) {
  final subscription = stream.listen((_) {});
  callbacks[0](subscription);
}

StreamSubscription<int> parenthesizedReturn(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  return (subscription);
}

void parenthesizedCancel(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  (subscription).cancel();
}

void parenthesizedInitializer(StreamSubscription<int> source) {
  final subscription = (source);
}

class ExplicitFieldTransfer {
  StreamSubscription<int>? transferredSubscription;
  void start(Stream<int> stream) {
    final local = stream.listen((_) {});
    this.transferredSubscription = local;
  }
  void dispose() => transferredSubscription?.cancel();
}

void otherReceiverTransfer(Stream<int> stream, ReleasedField receiver) {
  final subscription = stream.listen((_) {});
  receiver.subscription = subscription;
}

class FieldCancelTearoff {
  late StreamSubscription<int> subscription;
  void retainCancel() {
    final callback = subscription.cancel;
  }
}

class OtherSubscriptionOwner {
  StreamSubscription<int>? subscription;
  void dispose() => subscription?.cancel();
}

class SameNameIsNotSameField {
  StreamSubscription<int>? subscription;
  void transfer(OtherSubscriptionOwner other, StreamSubscription<int> value) {
    other.subscription = value;
  }
}

extension type SubscriptionView(StreamSubscription<int> value) {}

class ErasedSubscriptionField {
  SubscriptionView? subscription;
}

extension type SubscriptionInterface(StreamSubscription<int> value)
    implements StreamSubscription<int> {}

void extensionSubscriptionLocal(Stream<int> stream) {
  SubscriptionInterface subscription = SubscriptionInterface(stream.listen((_) {}));
}
Cancel this `StreamSubscription` instance.
3class Watcher {
4 StreamSubscription<int>? _subscription;
Cancel this `StreamSubscription` instance.
4 StreamSubscription<int>? _subscription;
5 late StreamSubscription<String> other;
Cancel this `StreamSubscription` instance.
12void listenLocally(Stream<int> stream) {
13 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
17void typedLocal(Stream<int> stream) {
18 StreamSubscription<int> subscription = stream.listen(print);
Cancel this `StreamSubscription` instance.
22void fromController(StreamController<int> controller) {
23 var subscription = controller.stream.listen((_) {});
Cancel this `StreamSubscription` instance.
28 final run = () {
29 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
35void aliasedDeclaration(Stream<int> stream) {
36 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
40void reassigned(Stream<int> stream, StreamSubscription<int>? holder) {
41 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
51void constructorArgument(Stream<int> stream) {
52 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
56void functionArgument(Stream<int> stream, void Function(StreamSubscription<int>) callback) {
57 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
61void assertedAlias(StreamSubscription<int>? source) {
62 final subscription = source!;
Cancel this `StreamSubscription` instance.
66void shadowedClosure(Stream<int> stream) {
67 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
72void localCascade(Stream<int> stream) {
73 final subscription = stream.listen((_) {})..pause();
Cancel this `StreamSubscription` instance.
76class ThroughThisPause {
77 StreamSubscription<int>? subscription;
Cancel this `StreamSubscription` instance.
81class LeakedField {
82 StreamSubscription<int>? subscription;
Cancel this `StreamSubscription` instance.
94void genericConstructorArgument(Stream<int> stream) {
95 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
102 factory FactorySubscriptionBox(Stream<T> stream) {
103 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
108void dynamicCallback(Stream<int> stream, dynamic callback) {
109 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
113void assertedCallback(Stream<int> stream, void Function(StreamSubscription<int>)? callback) {
114 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
118void immediateCallback(Stream<int> stream) {
119 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
123void functionCallback(Stream<int> stream, Function callback) {
124 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
132 void pass(Stream<int> stream) {
133 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
138void getterCallback(Stream<int> stream, CallbackBox box) {
139 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
149void chainedCascadeRelease() {
150 final subscription = createSubscription();
Cancel this `StreamSubscription` instance.
154void indexedCallback(Stream<int> stream, List<void Function(StreamSubscription<int>)> callbacks) {
155 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
159StreamSubscription<int> parenthesizedReturn(Stream<int> stream) {
160 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
164void parenthesizedCancel(Stream<int> stream) {
165 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
169void parenthesizedInitializer(StreamSubscription<int> source) {
170 final subscription = (source);
Cancel this `StreamSubscription` instance.
175 void start(Stream<int> stream) {
176 final local = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
182void otherReceiverTransfer(Stream<int> stream, ReleasedField receiver) {
183 final subscription = stream.listen((_) {});
Cancel this `StreamSubscription` instance.
187class FieldCancelTearoff {
188 late StreamSubscription<int> subscription;
Cancel this `StreamSubscription` instance.
199class SameNameIsNotSameField {
200 StreamSubscription<int>? subscription;
Cancel this `StreamSubscription` instance.
208class ErasedSubscriptionField {
209 SubscriptionView? subscription;
Cancel this `StreamSubscription` instance.
215void extensionSubscriptionLocal(Stream<int> stream) {
216 SubscriptionInterface subscription = SubscriptionInterface(stream.listen((_) {}));

Valid

example.dartdart
import 'dart:async';

void consume(StreamSubscription<int> subscription) {}

class Watcher {
  StreamSubscription<int>? _subscription;

  void start(Stream<int> stream) {
    _subscription = stream.listen((_) {});
  }

  Future<void> dispose() async {
    await _subscription?.cancel();
  }
}

class NullAsserted {
  StreamSubscription<int>? _subscription;

  void dispose() {
    _subscription!.cancel();
  }
}

class ThroughThis {
  StreamSubscription<int>? subscription;

  void dispose() {
    this.subscription?.cancel();
  }
}

class Paused {
  StreamSubscription<int>? _pausedSubscription;

  void start(Stream<int> stream) {
    _pausedSubscription = stream.listen((_) {});
  }

  void hold() {
    _pausedSubscription?.pause();
  }
}

class Injected {
  final StreamSubscription<int> subscription;

  Injected(this.subscription);
}

class Initialized {
  final StreamSubscription<int> subscription;

  Initialized(Stream<int> stream) : subscription = stream.listen((_) {});
}

void cancelled(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  subscription.cancel();
}

StreamSubscription<int> returned(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  return subscription;
}

void passed(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  consume(subscription);
}

void tornOff(Stream<int> stream, void Function(Future<void> Function()) register) {
  final subscription = stream.listen((_) {});
  register(subscription.cancel);
}

void cascaded(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  subscription
    ..pause()
    ..cancel();
}

void unresolved(dynamic stream) {
  final subscription = stream.listen((_) {});
  subscription.pause();
}

void cancelledFromClosure(Stream<int> stream) {
  final subscription = stream.listen((_) {});
  final dispose = () => subscription.cancel();
  dispose();
}

class CascadingField {
  StreamSubscription<int>? subscription;
  void hold() {
    subscription?..pause();
  }
}

abstract class ResourceBase {
  StreamSubscription<int>? subscription;
}

class ResourceChild extends ResourceBase {
  void dispose() => subscription?.cancel();
}

abstract class ThisResourceBase {
  StreamSubscription<int>? subscription;
}

class ThisResourceChild extends ThisResourceBase {
  void dispose() => this.subscription?.cancel();
}

void siblingCases(Stream<int> stream, int value) {
  switch (value) {
    case 1:
      final subscription = stream.listen((_) {});
      subscription.cancel();
    case 2:
      final subscription = stream.listen((_) {});
      subscription.cancel();
  }
}

void collectionScope(Stream<int> stream) {
  final values = [for (var subscription = 0; subscription < 1; subscription++) subscription];
  final subscription = stream.listen((_) {});
  subscription.cancel();
}

void nestedFormal(Stream<int> stream, void callback(StreamSubscription<int> subscription)) {
  final subscription = stream.listen((_) {});
  subscription.cancel();
}

Configure cancel-subscriptions

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

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "cancel-subscriptions": "error"
      }
    }
  }
}

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

example.dartdart
// falcon-ignore lint/correctness/cancel-subscriptions: <reason>