async_job_lints
async_job_lints is an analyzer plugin for code built on
async_job. It reports the mistakes that
compile, run and often pass a first test: a wait in cleanup that never happens,
a resource lost to a cancellation, a catch that resets a working device, a test
that never moves its fake clock, a wait that a cancellation does not end. Each
rule links to the page of async_job that shows the mistake and the right way.
Code built on solo or flutter_solo needs solo_lints instead: it includes these rules and adds those of the controller. Name one of the two plugins, not both: the analysis server resolves the versions of all plugins together, and when they disagree, no plugin runs.
Also on the documentation site, with search.
Install
Name the plugin in analysis_options.yaml at the root of the package or the
workspace:
plugins:
async_job_lints: ^0.1.1
The analysis server fetches the plugin itself, so it is not a dependency in
pubspec.yaml. Plugins need Dart 3.10 or later. dart analyze and the editor
report its diagnostics. Run dart analyze on the root of the package or on
single files: on a folder below the root, such as dart analyze lib, it shows
none of them. flutter analyze reports them on Flutter 3.38, but from Flutter
3.44 on it shows none of them; in a Flutter package, run dart analyze for the
plugin. An editor sees a change to the plugins section after its analysis
server restarts.
The warnings are on as soon as the plugin is named. The two lints,
catch_without_check and bare_await, are turned on by name:
plugins:
async_job_lints:
version: ^0.1.1
diagnostics:
catch_without_check: true
bare_await: true
A rule takes a severity in place of true: error, warning or info.
bare_await: warning turns the lint on as a warning, and dart analyze then
fails on it, as on any warning. false turns a warning off.
A diagnostic is silenced with the name of the plugin in front of the rule:
// ignore: async_job_lints/ctx_in_cleanup on the line above it, or
// ignore_for_file: async_job_lints/ctx_in_cleanup for the whole file.
The quick fixes are offered in the editor. dart fix does not apply them: it
applies the fixes of the analyzer itself, not those of plugins.
On Dart 3.12, dart analyze in a package that names a plugin prints each
diagnostic of the analyzer itself twice. Dart 3.13 prints it once.
The library package:async_job_lints/plugin_api.dart is for analyzer plugins
built on top of this one, such as solo_lints, and not for the code a plugin
checks.
Rules
| Rule | Kind | Reports |
|---|---|---|
ctx_in_cleanup |
warning | a waiting member of the context called in cleanup |
release_on_the_call |
warning | a release registered on the line under the call that brings the resource |
catch_without_check |
lint | a broad catch in a body that acts before asking the job |
sync_fake_async |
warning | an async callback of fakeAsync |
bare_await |
lint | an await in a job body that does not go through the context |
ctx_in_cleanup
ctx.onDispose(() async {
await ctx.join(() => device.flush()); // ctx_in_cleanup
});
Cleanup runs after the body, where join throws StateError, so the buffer
stays unflushed however the job ended. Wait there with a plain await. The
rule looks into the functions given to ctx.onDispose and ctx.onDiscard and
to dispose: and discard: of join, abandonable and run, literals, and
local functions and final variables holding a literal passed by name, and
reports the members that throw there: the checkpoints, onCancel, start and
each. onDispose, onDiscard, disown, log and unattended are left
alone. The checkpoints include those solo adds to its context, such as emit.
See
Cleanup.
release_on_the_call
final db = await ctx.join(Database.open);
ctx.onDispose(db.close); // release_on_the_call
A cancellation during the open throws before the second line, and the open
database is lost. Pass the release to the call that brings the resource:
ctx.join(Database.open, dispose: (db) => db.close()). The rule takes join,
abandonable and run with no release of their own, followed at once by
onDispose or onDiscard that mentions the variable. See
Registering on arrival.
The quick fix removes the onDispose or onDiscard and passes the release to
the call as dispose: or discard:, so the lines above become
ctx.join(Database.open, dispose: (db) => db.close()). It is offered for a
release that is a tear-off of a method of the variable, such as db.close, or
a function literal without parameters.
catch_without_check
try {
await ctx.join(hw.open);
await ctx.join(() => hw.setZoom(zoom));
} on Object { // catch_without_check
await hw.reset();
rethrow;
}
Cancelled implements Exception, so a catch without on, or on Object,
Exception or Outcome, takes it along with the failures it was written for,
and a cancelled job resets a camera nobody broke. Make ctx.check() the first
line of the catch. The rule looks at a job body, at the local functions
declared in it and at the actions it waits for through join, abandonable
and uncancellable; cleanup and unattended work are left alone. It is quiet
when the try block neither awaits nor reaches a checkpoint, and when the catch
begins with a checkpoint, after any if (...) rethrow; lines, begins with
if (ctx.job.isCancelled) rethrow;, or ends in rethrow; with no wait and no
checkpoint on the way, as a synchronous release or a log does. It is a lint
rather than a warning because a catch that has to wait for a release on a
cancellation too is reported as well: there ctx.check() first would leave the
resource behind, and the catch keeps its lines under an // ignore:. See
Catching errors of the operation.
The quick fix inserts ctx.check(); as the first line of the catch.
sync_fake_async
fakeAsync((async) async { // sync_fake_async
async.elapse(const Duration(milliseconds: 10));
await job.cancel();
expect(job.outcome, isA<Cancelled>());
});
An await in the callback suspends it, and nobody is left to move the fake
clock: the test hangs, or ends green with nothing run. Keep the callback
synchronous, move the clock with elapse and read job.outcome. The rule
reports the async of a callback given to fakeAsync or FakeAsync.run. See
A cancelled open.
bare_await
final job = Job<void>((ctx) async {
while (true) {
use(await ctx.abandonable(database.readAll));
await Future<void>.delayed(const Duration(seconds: 1)); // bare_await
}
});
A plain await does not hear a cancellation: the job is cancelled, and the
body still sits the delay out and goes on to the next line. Wait through the
context: ctx.pause instead of Future.delayed, ctx.each instead of
await for, which the rule reports with a message of its own, and for a call
ctx.join or ctx.abandonable. The two differ in what a cancellation does to
the call. join is for a call that must finish: it accepts the cancellation at
once, so onCancel can stop the call, waits for the call to come back and
throws only then. abandonable stops waiting at the cancellation and leaves
the call in flight. Steps that must not hear a cancellation until they end go
into ctx.uncancellable. See
The context at a glance
and
Letting time pass.
The rule looks at a job body, a function literal or a top-level function whose
first parameter is a JobContext, also when it is named _. Functions
declared inside the body are left alone: the actions of join and
uncancellable, cleanup, unattended work, local functions. A local function
is reported where the body awaits its call. The rule is quiet on a wait through
a member of the context of this body: join, abandonable, uncancellable,
pause, run and runAll. It is quiet on value, done and cancel() of a
child that ctx.each brings or that the body passed to ctx.start before in a
final variable or a final field, and on Future.wait, [...].wait and
(...).wait of such futures, a conditional of them and a final local variable
holding one. A call that passes the context to a parameter typed JobContext,
positional or named, waits through it. A release right after ctx.disown(v),
on the next line or under if (ctx.disown(v)), has to run for a cancellation
too. It is quiet on a value that is not a future, in finally, in a catch that
is on Cancelled, and in a catch that takes a cancellation, untyped,
on Object, on Exception or on Outcome, and ends in rethrow; with no
return and no break or continue out of it before. A check at the top of a
catch is not enough: it hears a cancellation that has come, not one that comes
during the wait after it. A body of a job made with cancellable: false, as a
literal or a constant, is left alone, and so is a top-level function every job
of which is made so.
A loop of plain waits inside one ctx.join(() async { ... }) is not reported,
although a cancellation does not end it either. A child held in a var, a
parameter or a field that is not final, and a ... in the list of
Future.wait, are reported though they wait through the context. So are a
local function that waits through the context itself, where the body awaits its
call, a future of the context taken further by timeout or then,
Future.any, a mapped list such as jobs.map(ctx.run) and a variable of a
pattern.
In a solo controller, a job that waits for a job of the controller's own queue
waits for itself: the job it waits for is queued behind it. This rule reports
that wait like any other plain one. solo_lints reports it as await_own_queue,
with a message about the queue, and keeps bare_await quiet there.
It is a lint rather than a warning because tests wait in bodies on purpose, to
cancel a job midway. A project that gates CI on it turns it on as
bare_await: warning and puts // ignore_for_file: async_job_lints/bare_await
into the test files that hold a body on a plain wait.
Versions
The plugin works on Dart 3.10 and later, with analyzer from 12.1 to 14. A
Dart SDK that needs analyzer 15 does not load this version, and its rules
stay silent until a release lifts the bound.
The rules know the types of async_job by their names and the library they come
from, without depending on the package, and the same way the checkpoints solo
adds to the context. On a version of those packages where a name has changed, a
rule stays silent rather than reports code that is right. The rules also know
wait, the name of abandonable before async_job 1.0, so they check code on
0.x the same way.
Libraries
- async_job_lints
- The rules of async_job_lints, registered by AsyncJobLintsPlugin.
- main
- The analyzer plugin of async_job_lints.
- plugin_api
- The API for an analyzer plugin built on top of async_job_lints.