Перейти к содержимому

async_job_lints

async_job_lints — плагин анализатора для кода на async_job. Он сообщает об ошибках, которые компилируются, работают и часто проходят первый тест: ожидание в уборке, которое так и не случается; ресурс, потерянный при отмене; catch, который сбрасывает исправное устройство; тест, который не двигает свои поддельные часы; ожидание, которое отмена не прерывает. Каждое правило ссылается на страницу async_job, где ошибка показана рядом с правильным способом.

Коду на solo или flutter_solo нужен вместо него solo_lints: он включает эти правила и добавляет правила контроллера. Называйте один из двух плагинов, а не оба: сервер анализа разрешает версии всех плагинов вместе, и если они не сходятся, не работает ни один плагин.

Назовите плагин в analysis_options.yaml в корне пакета или рабочего пространства:

plugins:
async_job_lints: ^0.1.1

Плагин сервер анализа скачивает сам, поэтому в pubspec.yaml он не зависимость. Плагинам нужен Dart 3.10 или новее. Его диагностики показывают dart analyze и редактор. Запускайте dart analyze для корня пакета или для отдельных файлов: для каталога ниже корня, например dart analyze lib, он не показывает ни одной. flutter analyze показывает их на Flutter 3.38, но начиная с Flutter 3.44 не показывает ни одной; во Flutter-пакете плагин проверяют через dart analyze. Правку раздела plugins редактор видит после перезапуска своего сервера анализа.

Предупреждения включаются, как только плагин назван. Два линта, catch_without_check и bare_await, включаются по имени:

plugins:
async_job_lints:
version: ^0.1.1
diagnostics:
catch_without_check: true
bare_await: true

Вместо true правилу можно дать уровень: error, warning или info. bare_await: warning включает линт как предупреждение, и тогда dart analyze на нём падает, как на любом предупреждении. false выключает предупреждение.

Диагностику глушат именем плагина перед именем правила: // ignore: async_job_lints/ctx_in_cleanup строкой выше или // ignore_for_file: async_job_lints/ctx_in_cleanup на весь файл.

Быстрые исправления предлагает редактор. dart fix их не применяет: он применяет исправления самого анализатора, а не плагинов.

На Dart 3.12 dart analyze в пакете, где назван плагин, печатает каждую диагностику самого анализатора дважды. Dart 3.13 печатает её один раз.

Библиотека package:async_job_lints/plugin_api.dart — для плагинов анализатора, построенных поверх этого, как solo_lints, а не для кода, который плагин проверяет.

Правило Вид Сообщает
ctx_in_cleanup предупреждение член контекста, который ждёт, вызван в уборке
release_on_the_call предупреждение освобождение зарегистрировано строкой ниже вызова, который приносит ресурс
catch_without_check линт широкий catch в теле, которое действует, не спросив задачу
sync_fake_async предупреждение async-колбэк fakeAsync
bare_await линт await в теле задачи, который идёт мимо контекста
ctx.onDispose(() async {
await ctx.join(() => device.flush()); // ctx_in_cleanup
});

Уборка идёт после тела, где join бросает StateError, поэтому буфер остаётся несброшенным, чем бы задача ни кончилась. Ждите там простым await. Правило смотрит в функции, переданные в ctx.onDispose и ctx.onDiscard и в dispose: и discard: методов join, abandonable и run, — литералы, а также локальные функции и final-переменные с литералом, переданные по имени, — и сообщает о членах, которые там бросают: о контрольных точках, onCancel, start и each. onDispose, onDiscard, disown, log и unattended оно не трогает. В контрольные точки входят и те, что solo добавляет своему контексту, например emit. Подробнее в разделе «dispose для того, что задача оставляет себе».

final db = await ctx.join(Database.open);
ctx.onDispose(db.close); // release_on_the_call

Отмена во время открытия бросает раньше второй строки, и открытая база теряется. Передайте освобождение вызову, который приносит ресурс: ctx.join(Database.open, dispose: (db) => db.close()). Правило берёт join, abandonable и run без своего освобождения, за которыми сразу идёт onDispose или onDiscard, упоминающий переменную. Подробнее в разделе «Регистрация по приходу».

Быстрое исправление удаляет onDispose или onDiscard и передаёт освобождение вызову как dispose: или discard:, так что строки выше превращаются в ctx.join(Database.open, dispose: (db) => db.close()). Исправление предлагается, когда освобождение — отрыв метода переменной, как db.close, или литерал функции без параметров.

try {
await ctx.join(hw.open);
await ctx.join(() => hw.setZoom(zoom));
} on Object { // catch_without_check
await hw.reset();
rethrow;
}

Cancelled реализует Exception, поэтому catch без on или с on на Object, Exception или Outcome ловит его вместе с ошибками, ради которых его писали, и отменённая задача сбрасывает камеру, которую никто не ломал. Сделайте ctx.check() первой строкой catch. Правило смотрит в тело задачи, в объявленные в нём локальные функции и в действия, которых тело ждёт через join, abandonable и uncancellable; уборку и работу без присмотра оно не трогает. Правило молчит, когда блок try ничего не ждёт и не доходит до контрольной точки, а также когда catch начинается с контрольной точки после любых строк if (...) rethrow;, начинается с if (ctx.job.isCancelled) rethrow; или кончается rethrow;, ничего не ожидая и не задевая контрольных точек по пути, — так поступают синхронное освобождение и запись в лог. Это линт, а не предупреждение: о catch, которому и при отмене нужно дождаться освобождения, правило тоже сообщает. Там ctx.check() первой строкой оставил бы ресурс неосвобождённым, и такой catch сохраняет свои строки под // ignore:. Подробнее в разделе «Перехват ошибок операции».

Быстрое исправление вставляет ctx.check(); первой строкой catch.

fakeAsync((async) async { // sync_fake_async
async.elapse(const Duration(milliseconds: 10));
await job.cancel();
expect(job.outcome, isA<Cancelled>());
});

await в колбэке приостанавливает его, и двигать поддельные часы больше некому: тест висит или кончается зелёным, ничего не выполнив. Оставьте колбэк синхронным, двигайте часы через elapse и читайте job.outcome. Правило сообщает об async колбэка, переданного в fakeAsync или FakeAsync.run. Подробнее в разделе «Отменённое открытие».

final job = Job<void>((ctx) async {
while (true) {
use(await ctx.abandonable(database.readAll));
await Future<void>.delayed(const Duration(seconds: 1)); // bare_await
}
});

Простой await отмену не слышит: задачу отменили, а тело всё пережидает задержку и переходит к следующей строке. Ждите через контекст: ctx.pause вместо Future.delayed, ctx.each вместо await for — о нём правило сообщает своим сообщением, — а для вызова ctx.join или ctx.abandonable. Они различаются тем, что отмена делает с вызовом. join — для вызова, который должен закончиться: он принимает отмену сразу, так что onCancel может остановить вызов, ждёт, пока вызов вернётся, и только тогда бросает. abandonable перестаёт ждать при отмене и оставляет вызов в полёте. Шаги, которые не должны слышать отмену, пока не закончатся, кладите в ctx.uncancellable. Подробнее — в разделе «Контекст коротко» справочника async_job и в разделе «Дать времени пройти» его страницы о времени.

Правило смотрит в тело задачи — литерал функции или функцию верхнего уровня, первый параметр которой JobContext, в том числе с именем _. Функции, объявленные внутри тела, оно не трогает: действия join и uncancellable, уборку, работу unattended, локальные функции. О локальной функции правило сообщает там, где тело ждёт её вызова. Правило молчит об ожидании через член контекста этого тела: join, abandonable, uncancellable, pause, run и runAll. Молчит о value, done и cancel() ребёнка, которого приносит ctx.each или которого тело раньше передало в ctx.start в финальной переменной или финальном поле, и о Future.wait, [...].wait и (...).wait таких future, об условном выражении из них и о final-переменной, которая держит такое. Вызов, который передаёт контекст параметру типа JobContext, позиционному или именованному, ждёт через него. Освобождение сразу после ctx.disown(v), следующей строкой или под if (ctx.disown(v)), должно выполниться и при отмене. Правило молчит о значении, которое не future, в finally, в catch с on Cancelled и в catch, который ловит отмену, — без типа, on Object, on Exception или on Outcome, — и кончается rethrow;, а до него из него не уходят ни return, ни break или continue. Проверки в начале catch мало: она слышит отмену, которая уже пришла, а не ту, что придёт во время ожидания после неё. Тело задачи с cancellable: false, литералом или константой, правило не трогает, как и функцию верхнего уровня, все задачи которой созданы так.

Цикл простых ожиданий внутри одного ctx.join(() async { ... }) правило не замечает, хотя отмена не прерывает и его. Ребёнок в var, в параметре или в нефинальном поле и ... в списке Future.wait правило называет, хотя они ждут через контекст. Называет оно и локальную функцию, которая сама ждёт через контекст, — там, где тело ждёт её вызова, — и future контекста, продолженный timeout или then, Future.any, отображённый список вроде jobs.map(ctx.run) и переменную шаблона.

В контроллере solo задача, которая ждёт задачу из очереди своего же контроллера, ждёт саму себя: ожидаемая задача стоит в очереди за ней. Это правило сообщает о таком ожидании, как о любом простом. solo_lints сообщает о нём как await_own_queue, сообщением об очереди, и bare_await там молчит.

Это линт, а не предупреждение: тесты ждут в телах намеренно, чтобы отменить задачу посередине. Проект, который держит на нём CI, включает его как bare_await: warning и ставит // ignore_for_file: async_job_lints/bare_await в файлы тестов, где тело стоит на простом ожидании.

Плагин работает на Dart 3.10 и новее, с analyzer от 12.1 до 14. SDK Dart, которому нужен analyzer 15, эту версию не загружает, и её правила молчат, пока выпуск не поднимет границу.

Типы async_job правила узнают по именам и библиотеке, откуда те пришли, и от пакета не зависят; так же они узнают контрольные точки, которые solo добавляет контексту. На версии этих пакетов, где имя сменилось, правило молчит, а не сообщает о правильном коде. Правила знают и wait — имя abandonable до async_job 1.0, — поэтому код на 0.x проверяют так же.