dispose-fields

Path
lint/correctness/dispose-fields
Default
On (recommended)
Origin
Falcon
Domain
flutter
Rule
221 of 512
recommendedflutter

Requires other owned disposable State fields to be released in dispose().

Fields initialized or assigned a resolved constructor with an instance dispose method must receive an eagerly evaluated dispose() call in the State's dispose() method, including calls wrapped in arguments or operands. Borrowed fields, static fields, unresolved types, and conditional-only releases are not treated as proof of ownership or cleanup. TextEditingController, AnimationController, ScrollController, PageController, and TabController belong to proper-controller-dispose, FocusNode/Timer to dispose-focus-node-and-timer, subscriptions to cancel-subscriptions, and sinks to close-sinks. Unretained temporaries belong to avoid-undisposed-instances. Calls after a possible return/throw are not proof, including idempotence guards whose cross-invocation state is outside this rule's analysis. Deferred closures and short-circuit right operands do not prove cleanup.

Invalid

example.dartdart
import 'package:flutter/material.dart';
class Resource {
 bool dispose() => true;
 void accept(dynamic value) {}
}
class A extends State<StatefulWidget> {
 final a = Resource();
 final b = Resource();
 final c = Resource();
 final d = Resource();
 final e = Resource();
 void dispose() {
   a.dispose;
   if (false) b.dispose();
   final callback = () => c.dispose();
   super.dispose();
 }
}

class DeferredCleanup extends State<StatefulWidget> {
 final deferred = Resource();
 final shortCircuit = Resource();
 final conditional = Resource();
 bool enabled = false;
 @override
 void dispose() {
   consume(() => deferred.dispose());
   enabled && shortCircuit.dispose();
   enabled ? conditional.dispose() : false;
   super.dispose();
 }
}

class ResourceHolder {
 final resource = Resource();
 final values = <bool?>[];
 dynamic pending;
}

class OptionalCleanup extends State<StatefulWidget> {
 final coalesced = Resource();
 final disjunction = Resource();
 final optionalArgument = Resource();
 final collectionIf = Resource();
 final collectionFor = Resource();
 final mapIf = Resource();
 final mapFor = Resource();
 final coalescingAssignment = Resource();
 final optionalAssignment = Resource();
 final chainedArgument = Resource();
 final chainedIndex = Resource();
 final optionalCascade = Resource();
 final optionalIndex = Resource();
 final optionalIndexAssignment = Resource();
 final cascadeAssignment = Resource();
 final cascadeIndexAssignment = Resource();
 final switchArm = Resource();
 final nullAwareMapKey = Resource();
 final assertedChain = Resource();
 String? maybeKey;
 Resource? services;
 ResourceHolder? holder;
 final cascadeHolder = ResourceHolder();
 List<bool?>? values;
 final cascadeValues = <bool?>[null];
 bool? pending;
 bool enabled = false;
 @override
 void dispose() {
   services?.dispose() ?? coalesced.dispose();
   enabled || disjunction.dispose();
   services?.accept(optionalArgument.dispose());
   consume([if (enabled) collectionIf.dispose()]);
   consume([for (final value in <int>[]) collectionFor.dispose()]);
   consume({if (enabled) 'key': mapIf.dispose()});
   consume({for (final value in <int>[]) value: mapFor.dispose()});
   consume({?maybeKey: nullAwareMapKey.dispose()});
   pending ??= coalescingAssignment.dispose();
   holder?.pending = optionalAssignment.dispose();
   holder?.resource.accept(chainedArgument.dispose());
   holder?.resource!.accept(assertedChain.dispose());
   consume(holder?.values[chainedIndex.dispose() ? 0 : 1]);
   services?..accept(optionalCascade.dispose());
   consume(values?[optionalIndex.dispose() ? 0 : 1]);
   values?[0] = optionalIndexAssignment.dispose();
   cascadeHolder..pending ??= cascadeAssignment.dispose();
   cascadeValues..[0] ??= cascadeIndexAssignment.dispose();
   consume(switch (enabled) { _ => switchArm.dispose() });
   super.dispose();
 }
}

class ThrowingArgumentCleanup extends State<StatefulWidget> {
 final afterThrow = Resource();
 bool enabled = false;
 @override
 void dispose() {
   consumePair(enabled ? throw StateError('stop') : null, afterThrow.dispose());
   super.dispose();
 }
}

class ThrowingCascadeCleanup extends State<StatefulWidget> {
 final resource = Resource();
 bool enabled = false;
 @override
 void dispose() {
   resource..accept(enabled ? null : throw StateError('stop'))..dispose();
   super.dispose();
 }
}

void consumePair(dynamic first, dynamic second) {}

void consume(dynamic value) {}
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
6class A extends State<StatefulWidget> {
7 final a = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
7 final a = Resource();
8 final b = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
8 final b = Resource();
9 final c = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
9 final c = Resource();
10 final d = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
10 final d = Resource();
11 final e = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
20class DeferredCleanup extends State<StatefulWidget> {
21 final deferred = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
21 final deferred = Resource();
22 final shortCircuit = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
22 final shortCircuit = Resource();
23 final conditional = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
40class OptionalCleanup extends State<StatefulWidget> {
41 final coalesced = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
41 final coalesced = Resource();
42 final disjunction = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
42 final disjunction = Resource();
43 final optionalArgument = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
43 final optionalArgument = Resource();
44 final collectionIf = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
44 final collectionIf = Resource();
45 final collectionFor = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
45 final collectionFor = Resource();
46 final mapIf = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
46 final mapIf = Resource();
47 final mapFor = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
47 final mapFor = Resource();
48 final coalescingAssignment = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
48 final coalescingAssignment = Resource();
49 final optionalAssignment = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
49 final optionalAssignment = Resource();
50 final chainedArgument = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
50 final chainedArgument = Resource();
51 final chainedIndex = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
51 final chainedIndex = Resource();
52 final optionalCascade = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
52 final optionalCascade = Resource();
53 final optionalIndex = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
53 final optionalIndex = Resource();
54 final optionalIndexAssignment = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
54 final optionalIndexAssignment = Resource();
55 final cascadeAssignment = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
55 final cascadeAssignment = Resource();
56 final cascadeIndexAssignment = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
56 final cascadeIndexAssignment = Resource();
57 final switchArm = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
57 final switchArm = Resource();
58 final nullAwareMapKey = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
58 final nullAwareMapKey = Resource();
59 final assertedChain = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
93class ThrowingArgumentCleanup extends State<StatefulWidget> {
94 final afterThrow = Resource();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
103class ThrowingCascadeCleanup extends State<StatefulWidget> {
104 final resource = Resource();

Valid

example.dartdart
import 'dart:async';
import 'package:flutter/material.dart';
class Resource {
 void dispose() {}
 static Resource sharedInstance() => Resource();
}
class A extends State<StatefulWidget> {
 final a = Resource();
 final b = Resource();
 final controller = TextEditingController();
 final focus = FocusNode();
 final timer = Timer(Duration.zero, () {});
 final cascadedController = TextEditingController()..text = '';
 final singleton = Resource.sharedInstance();
 static final shared = Resource();
 Resource? borrowed;
 void dispose() {
   a.dispose();
   this.b.dispose();
   controller.dispose();
   focus.dispose();
   timer.cancel();
   super.dispose();
 }
}
class Other { final resource = Resource(); }

class Services {
 Future<void> dispose() async {}
 void accept(dynamic value) {}
}

class EagerArgumentCleanup extends State<StatefulWidget> {
 final Services? _services = Services();
 final resource = Services();
 final cascaded = Services();
 final Services? nullableCascaded = Services();
 final eagerCollection = Services();
 final eagerAssignment = Services();
 dynamic pending;
 @override
 void dispose() {
   unawaited(_services?.dispose() ?? Future<void>.value());
   consume(resource.dispose());
   cascaded..dispose();
   nullableCascaded?..dispose();
   consume([eagerCollection.dispose()]);
   pending = eagerAssignment.dispose();
   super.dispose();
 }
}

class CleanupBeforeThrow extends State<StatefulWidget> {
 final resource = Services();
 bool enabled = false;
 @override
 void dispose() {
   consumePair(resource.dispose(), enabled ? throw StateError('stop') : null);
   super.dispose();
 }
}

class CascadeCleanupBeforeThrow extends State<StatefulWidget> {
 final resource = Services();
 bool enabled = false;
 @override
 void dispose() {
   resource..dispose()..accept(enabled ? null : throw StateError('stop'));
   super.dispose();
 }
}

class ParenthesizedReceiverCleanup extends State<StatefulWidget> {
 final resource = Services();
 dynamic receiver;
 @override
 void dispose() {
   (receiver?.resource).accept(resource.dispose());
   super.dispose();
 }
}

void consumePair(dynamic first, dynamic second) {}

void consume(dynamic value) {}

Configure dispose-fields

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

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "dispose-fields": "error"
      }
    }
  }
}

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

example.dartdart
// falcon-ignore lint/correctness/dispose-fields: <reason>