dispose-focus-node-and-timer

Path
lint/correctness/dispose-focus-node-and-timer
Default
On (recommended)
Origin
Falcon
Domain
flutter
Rule
222 of 512
recommendedflutter

Requires owned FocusNode and Timer fields to be released in State.dispose().

Only fields constructed by a resolved Flutter FocusNode or dart:async Timer constructor are checked, including assignments in lifecycle methods. A direct dispose()/cancel() invocation in dispose() is required; tear-offs, callbacks, and conditional-only invocations do not prove release. Borrowed and static fields are ignored. Controllers belong to proper-controller-dispose; other disposable fields belong to dispose-fields, and unretained temporary objects 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.

Invalid

example.dartdart
import 'dart:async';
import 'package:flutter/material.dart';
class A extends State<StatefulWidget> {
  final focus = FocusNode();
  final timer = Timer(Duration.zero, () {});
  final periodic = Timer.periodic(Duration.zero, (_) {});
  final tearoff = FocusNode();
  final closure = FocusNode();
  final conditional = FocusNode();
  void dispose() {
    tearoff.dispose;
    final callback = () => closure.dispose();
    if (false) conditional.dispose();
    super.dispose();
  }
}
class EarlyReturn extends State<StatefulWidget> {
 final focus = FocusNode();
 final timer = Timer(Duration.zero, () {});
 void dispose() {
   if (mounted) return;
   focus.dispose();
   timer.cancel();
   super.dispose();
 }
}
class ThrowExpression extends State<StatefulWidget> {
 final focus = FocusNode();
 void dispose() {
   final check = mounted ? null : throw StateError('not mounted');
   focus.dispose();
   super.dispose();
 }
}
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
3class A extends State<StatefulWidget> {
4 final focus = FocusNode();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
4 final focus = FocusNode();
5 final timer = Timer(Duration.zero, () {});
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
5 final timer = Timer(Duration.zero, () {});
6 final periodic = Timer.periodic(Duration.zero, (_) {});
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
6 final periodic = Timer.periodic(Duration.zero, (_) {});
7 final tearoff = FocusNode();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
7 final tearoff = FocusNode();
8 final closure = FocusNode();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
8 final closure = FocusNode();
9 final conditional = FocusNode();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
17class EarlyReturn extends State<StatefulWidget> {
18 final focus = FocusNode();
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
18 final focus = FocusNode();
19 final timer = Timer(Duration.zero, () {});
This owned field is never released; call its `dispose()` or `cancel()` method in `dispose()`.
27class ThrowExpression extends State<StatefulWidget> {
28 final focus = FocusNode();

Valid

example.dartdart
import 'dart:async';
import 'package:flutter/material.dart';
class A extends State<StatefulWidget> {
  final focus = FocusNode();
  final timer = Timer(Duration.zero, () {});
  final periodic = Timer.periodic(Duration.zero, (_) {});
  final controller = TextEditingController();
  static final shared = FocusNode();
  FocusNode? borrowed;
  void dispose() {
    focus.dispose();
    timer.cancel();
    periodic.cancel();
    controller.dispose();
    super.dispose();
  }
}
class Other { final focus = FocusNode(); }
class Arrow extends State<StatefulWidget> {
 final focus = FocusNode();
 void dispose() => focus.dispose();
}
class OrderIndependent extends State<StatefulWidget> {
 Timer? timer;
 FocusNode? focus = FocusNode();
 void dispose() {
   timer!.cancel();
   focus!.dispose();
   super.dispose();
 }
 void start() { timer = Timer.periodic(Duration.zero, (_) {}); }
}
class NestedReturn extends State<StatefulWidget> {
 final focus = FocusNode();
 void dispose() {
   void nested() { return; }
   focus.dispose();
   super.dispose();
 }
}

Configure dispose-focus-node-and-timer

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

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "dispose-focus-node-and-timer": "error"
      }
    }
  }
}

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

example.dartdart
// falcon-ignore lint/correctness/dispose-focus-node-and-timer: <reason>