dispose-fields
- Group
- Correctness
- Path
lint/correctness/dispose-fields- Default
- On (recommended)
- Origin
- Falcon
- Domain
- flutter
- Rule
- 221 of 512
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
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) {}class A extends State<StatefulWidget> { final a = Resource(); final a = Resource(); final b = Resource(); final b = Resource(); final c = Resource(); final c = Resource(); final d = Resource(); final d = Resource(); final e = Resource();class DeferredCleanup extends State<StatefulWidget> { final deferred = Resource(); final deferred = Resource(); final shortCircuit = Resource(); final shortCircuit = Resource(); final conditional = Resource();class OptionalCleanup extends State<StatefulWidget> { final coalesced = Resource(); final coalesced = Resource(); final disjunction = Resource(); final disjunction = Resource(); final optionalArgument = Resource(); final optionalArgument = Resource(); final collectionIf = Resource(); final collectionIf = Resource(); final collectionFor = Resource(); final collectionFor = Resource(); final mapIf = Resource(); final mapIf = Resource(); final mapFor = Resource(); final mapFor = Resource(); final coalescingAssignment = Resource(); final coalescingAssignment = Resource(); final optionalAssignment = Resource(); final optionalAssignment = Resource(); final chainedArgument = Resource(); final chainedArgument = Resource(); final chainedIndex = Resource(); final chainedIndex = Resource(); final optionalCascade = Resource(); final optionalCascade = Resource(); final optionalIndex = Resource(); final optionalIndex = Resource(); final optionalIndexAssignment = Resource(); final optionalIndexAssignment = Resource(); final cascadeAssignment = Resource(); final cascadeAssignment = Resource(); final cascadeIndexAssignment = Resource(); final cascadeIndexAssignment = Resource(); final switchArm = Resource(); final switchArm = Resource(); final nullAwareMapKey = Resource(); final nullAwareMapKey = Resource(); final assertedChain = Resource();class ThrowingArgumentCleanup extends State<StatefulWidget> { final afterThrow = Resource();class ThrowingCascadeCleanup extends State<StatefulWidget> { final resource = Resource();Valid
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.
{
"linter": {
"rules": {
"correctness": {
"dispose-fields": "error"
}
}
}
}Or silence one occurrence with a suppression comment; the reason is required.
// falcon-ignore lint/correctness/dispose-fields: <reason>More correctness rules
- dispose-focus-node-and-timerrecommendedflutter
Requires owned
FocusNodeand Timer fields to be released inState.dispose(). - dispose-provided-instancesrecommendedriverpod
Flags provider-created instances with a dispose method that lack ref.onDispose cleanup.
- dispose-provider-resourcesrecommendedriverpod
Flags provider-created resources without autoDispose or ref.onDispose cleanup.
- dispose-providersrecommendedprovider
Requires cleanup callbacks for disposable values created by package:provider's Provider.
- emit-new-bloc-state-instancesrecommendedbloc
Flags Bloc and Cubit emissions that reuse their current state instance.
- exhaustive-cases
exhaustive_casesrecommendedRequire switch statements over enum-like classes to cover every constant value.
- function-always-returns-null
Flags value functions with a proven explicit null return on every path.
- function-always-returns-same-value
Warns when distinct return paths in a function produce the same scalar constant.