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

Правила анализатора

Valkarn Tasks поставляется с двумя пакетами анализаторов Roslyn, которые активируются автоматически при импорте пакета:

  • UnaPartidaMas.Valkarn.Tasks.SourceGen.dll — правила для корректности ValkarnTask и жизненного цикла Unity. Идентификаторы правил начинаются с TT.
  • UnaPartidaMas.Valkarn.Tasks.Analyzer.dll — правила миграции для кодовых баз, переходящих с UniTask. Идентификаторы правил начинаются с MIG. Эти правила срабатывают только при наличии Cysharp.Threading.Tasks.UniTask в компиляции, поэтому в новых проектах они молчат.

Оба пакета — предварительно скомпилированные DLL, расположенные в папке Analyzers/ пакета. Unity загружает их как анализаторы Roslyn через ссылку .asmdef; проект _TestRunner~ загружает их через элементы <Analyzer> в TestRunner.csproj.


Правила корректности (TT)

Эти правила выявляют ошибки, связанные с природой единственного потребления ValkarnTask и неправильным использованием fire-and-forget.


TT001 — ValkarnTask уже ожидался

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

ValkarnTask рассчитан на единственное потребление: после ожидания внутренний токен устаревает. Второй await той же переменной встретит устаревший токен и выбросит исключение. Анализатор обнаруживает, когда одна и та же локальная переменная, параметр или поле типа ValkarnTask/ValkarnTask<T> ожидается более одного раза внутри одного метода.

Срабатывает на:

async ValkarnTask Bad()
{
ValkarnTask work = DoWorkAsync();
await work; // первый await — нормально
await work; // TT001: уже ожидался
}

Исправление: если нужно ветвление по результату, захватите его через .AsResult() до первого await, или перестройте код так, чтобы задача ожидалась ровно один раз.

async ValkarnTask Good()
{
var result = await DoWorkAsync().AsResult();
// используем result в обоих ветках
}

TT002 — ValkarnTask не ожидался и не отброшен

СвойствоЗначение
СерьёзностьError
КатегорияValkarnTask
Исправление кодаНет

Вызов метода, возвращающего ValkarnTask, использованный как выражение-оператор — без await, без присваивания и без последующего .Forget() — является скрытой ошибкой. Исключения, выброшенные внутри задачи, никогда не наблюдаются, а пулируемый конечный автомат задачи никогда не возвращается в пул.

Анализатор проверяет выражения-операторы, где тип выражения разрешается в ValkarnTask или ValkarnTask<T>. Пропускает:

  • Выражения присваивания (tasks[i] = DoWork())
  • Цепочки, оканчивающиеся на .Forget() (намеренный fire-and-forget)

Срабатывает на:

void Bad()
{
LoadDataAsync(); // TT002: не ожидался и не отброшен
ProcessItemAsync(); // TT002
}

Исправление — ожидать:

async ValkarnTask Good()
{
await LoadDataAsync();
await ProcessItemAsync();
}

Исправление — явный fire-and-forget:

void GoodFireAndForget()
{
LoadDataAsync().Forget();
}

TT013 — ValkarnTask возвращён, но никогда не потреблён

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

Этот идентификатор правила зарезервирован для будущего анализа потока данных. Предназначен для обнаружения паттерна «присвоено, но никогда не ожидалось» — сохранение ValkarnTask в переменную и последующее неожидание и неотбрасывание её — что TT002 не охватывает, поскольку TT002 проверяет только голые выражения-операторы.

Текущая реализация не регистрирует синтаксических действий. Когда анализ потока данных будет реализован, TT013 дополнит TT002, охватывая:

async ValkarnTask Bad()
{
ValkarnTask task = DoWorkAsync(); // присвоено, но никогда не ожидалось
DoOtherThing();
// task заброшен — TT013 (в будущем)
}

Методы, украшенные [FireAndForget], будут освобождены от этого правила.


Правила жизненного цикла (TT)

Эти правила связаны с управлением временем жизни MonoBehaviour и отменой.


TT010 — Авто-отмена активна для async-метода MonoBehaviour

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTask
Исправление кодаНет

Информационная подсказка. Любой async ValkarnTask (или async ValkarnTask<T>) метод внутри класса, наследующего от UnityEngine.MonoBehaviour, будет автоматически отменён при уничтожении объекта, если только метод не украшен [NoAutoCancel]. Этот диагностический вывод делает такое поведение видимым в IDE без необходимости помнить о нём.

Срабатывает на:

public class MyBehaviour : MonoBehaviour
{
async ValkarnTask LoadLevel() // TT010: авто-отмена активна
{
await ValkarnTask.Delay(2000);
}
}

Для отказа (и подавления TT010 для этого метода) добавьте [NoAutoCancel]:

[NoAutoCancel]
async ValkarnTask LoadLevel(CancellationToken ct)
{
await ValkarnTask.Delay(2000, ct);
}

TT011 — WhenAll смешивает разные области времени жизни

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

ValkarnTask.WhenAll, вызванный с задачами из разных областей времени жизни, может привести к неожиданному поведению частичной отмены. Если одна задача привязана к MonoBehaviour (возвращена методом экземпляра класса, наследующего MonoBehaviour), а другая не привязана (статическая задача или из класса, не являющегося MonoBehaviour), уничтожение объекта отменит одну задачу, но не другую, оставив комбинатор в неопределённом состоянии.

Срабатывает на:

// Предполагая EnemyAI : MonoBehaviour
await ValkarnTask.WhenAll(
enemyAI.PatrolAsync(), // привязана: автоматически отменяется при Destroy
GlobalMusic.FadeAsync() // не привязана: живёт бесконечно
);
// TT011: WhenAll смешивает времена жизни — PatrolAsync() против GlobalMusic.FadeAsync()

Исправление — дать обеим задачам общий токен отмены:

using var cts = new CancellationTokenSource();
await ValkarnTask.WhenAll(
enemyAI.PatrolAsync(cts.Token),
GlobalMusic.FadeAsync(cts.Token)
);

TT012 — Async-цикл без проверки отмены

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

Метод async ValkarnTask, содержащий цикл for, while, foreach или do-while, тело которого не имеет проверки отмены, является «зомби-циклом»: если срабатывает отмена жизненного цикла (например, MonoBehaviour уничтожен), сигнал авто-отмены не может прервать цикл. Цикл продолжает выполняться, даже если владеющий объект уже уничтожен.

Анализатор считает, что тело цикла имеет проверку отмены, если оно содержит любое из:

  • Выражение await (ожидаемая операция может наблюдать токен и выбросить OperationCanceledException)
  • ThrowIfCancellationRequested (как идентификатор или доступ к члену)
  • IsCancellationRequested (как идентификатор или доступ к члену)

Проверка не спускается во вложенные лямбды или локальные функции.

Срабатывает на:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true) // TT012: нет проверки отмены в теле
{
ProcessNextItem();
Thread.Sleep(16); // синхронное — не await
}
}

Исправление — добавить await или явную проверку:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true)
{
ct.ThrowIfCancellationRequested();
ProcessNextItem();
await ValkarnTask.Yield(); // также удовлетворяет проверке
}
}

TT014 — [NoAutoCancel] без параметра CancellationToken

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

[NoAutoCancel] на async ValkarnTask-методе в MonoBehaviour отказывает методу в автоматической отмене жизненного цикла. Но если метод не имеет параметра CancellationToken, у него нет механизма для наблюдения отмены вообще, что делает атрибут бессмысленным и почти наверняка указывает на забытый параметр.

Срабатывает на:

public class Enemy : MonoBehaviour
{
[NoAutoCancel]
async ValkarnTask Chase() // TT014: [NoAutoCancel] без параметра CancellationToken
{
await ValkarnTask.Delay(1000);
}
}

Исправление — добавить параметр CancellationToken:

[NoAutoCancel]
async ValkarnTask Chase(CancellationToken ct)
{
await ValkarnTask.Delay(1000, ct);
}

Информационные правила (TT)


TT015 — Сгенерирован мост-адаптер для Awaitable

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTask
Исправление кодаНет

Когда вы await Unity Awaitable (из UnityEngine) внутри async ValkarnTask-метода, Valkarn Tasks автоматически генерирует мост-адаптер через AwaitableBridge. Этот диагностический вывод информационный: он подтверждает, что мост активен. Никаких действий не требуется.

Срабатывает на:

async ValkarnTask LoadScene()
{
await SceneManager.LoadSceneAsync("Main"); // TT015: сгенерирован мост-адаптер
}

Никаких изменений не нужно. Мост обрабатывает преобразование прозрачно.


Правила качества кода (TT)


TT016 — Async-метод без await

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

Метод async ValkarnTask (или async ValkarnTask<T>), не содержащий выражений await — включая await foreach, await using и await внутри объявлений using — несёт полные накладные расходы на аллокацию конечного автомата без какой-либо пользы. Компилятор всё равно генерирует конечный автомат, но поскольку точки приостановки нет, метод всегда завершается синхронно.

Анализатор проверяет: AwaitExpressionSyntax, foreach с ключевым словом await, объявления using с ключевым словом await и операторы using с ключевым словом await.

Срабатывает на:

async ValkarnTask<int> ComputeTotal()  // TT016: нет await в теле
{
return items.Sum(x => x.Value);
}

Исправление — удалить async и вернуть завершённую задачу:

ValkarnTask<int> ComputeTotal()
{
return ValkarnTask.FromResult(items.Sum(x => x.Value));
}

Или для void-возврата:

ValkarnTask DoSetup()
{
Initialize();
return ValkarnTask.CompletedTask;
}

TT017 — [FireAndForget] на ValkarnTask<T>

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTask
Исправление кодаНет

[FireAndForget] сигнализирует, что метод намеренно запускается без ожидания его результата. Применение его к методу, возвращающему ValkarnTask<T> (типизированный результат), всегда отбрасывает T, делая типизированный возврат бессмысленным. Метод должен возвращать ValkarnTask (void) вместо этого.

Срабатывает на:

[FireAndForget]
async ValkarnTask<int> SendReport() // TT017: возвращаемое значение отбрасывается
{
await UploadAsync();
return 42; // это 42 никогда не увидят
}

Исправление — изменить тип возврата на ValkarnTask:

[FireAndForget]
async ValkarnTask SendReport()
{
await UploadAsync();
}

Правила миграции (MIG)

Анализатор миграции активируется только при наличии типа Cysharp.Threading.Tasks.UniTask в компиляции. Правила носят информационный или предупредительный характер для сопровождения перехода с UniTask на Valkarn Tasks. Ни одно из этих правил не имеет автоисправлений; описания ниже объясняют ручное изменение.


MIG001 — Обнаружен тип UniTask

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Обнаружено использование типа UniTask (самой структуры или UniTask<T>). Замените его на эквивалент ValkarnTask или ValkarnTask<T>.

// До
UniTask<Sprite> LoadSprite(string path) { ... }

// После
ValkarnTask<Sprite> LoadSprite(string path) { ... }

MIG002 — Параметр cancelImmediately не нужен

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Delay и другие методы UniTask, основанные на времени, принимают параметр cancelImmediately для быстрой отмены. Valkarn Tasks отменяет немедленно по умолчанию — параметра cancelImmediately нет. Удалите аргумент.

// До
await UniTask.Delay(1000, cancelImmediately: true, cancellationToken: ct);

// После
await ValkarnTask.Delay(1000, ct);

MIG003 — Обнаружен SuppressCancellationThrow()

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

.SuppressCancellationThrow() из UniTask преобразует отменённую задачу в кортеж (bool isCancelled, T result) без выброса исключения. Valkarn Tasks использует .AsResult() для той же цели, возвращая структуру Result<T>.

// До
var (isCancelled, value) = await myUniTask.SuppressCancellationThrow();

// После
var result = await myValkarnTask.AsResult();
if (result.IsCanceled) { ... }
T value = result.Value;

MIG004 — Обнаружен тип возврата Awaitable

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Метод возвращает тип Awaitable от Unity. Рассмотрите замену на ValkarnTask, который нативно интегрируется со средой выполнения Valkarn Tasks и поддерживает авто-отмену, пулирование и полный набор комбинаторов.


MIG005 — Обнаружен канал SingleConsumerUnbounded

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

Channel.CreateSingleConsumerUnbounded<T>() из UniTask создаёт канал без обратного давления. Рассмотрите замену на ValkarnTask.Channel.CreateBounded<T>(capacity) для добавления обратного давления и предотвращения неограниченного роста памяти под нагрузкой.

// До
var ch = Channel.CreateSingleConsumerUnbounded<Event>();

// После (с обратным давлением)
var ch = ValkarnTask.Channel.CreateBounded<Event>(capacity: 256);

// Или если неограниченность намеренна
var ch = ValkarnTask.Channel.CreateUnbounded<Event>();

MIG006 — Обнаружен UniTask PlayerLoopTiming

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Обнаружено значение enum PlayerLoopTiming из пространства имён Cysharp.Threading.Tasks. Измените директиву using на UnaPartidaMas.Valkarn.Tasks; имена значений enum идентичны.

// До
using Cysharp.Threading.Tasks;
timing = PlayerLoopTiming.Update;

// После
using UnaPartidaMas.Valkarn.Tasks;
timing = PlayerLoopTiming.Update; // то же имя, другое пространство имён

MIG007 — Обнаружен async UniTaskVoid

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

async UniTaskVoid был паттерном fire-and-forget метода в UniTask. Valkarn Tasks заменяет его двумя вариантами:

// До
async UniTaskVoid StartLoadAsync() { ... }

// После — вариант 1: атрибут [FireAndForget]
[FireAndForget]
async ValkarnTask StartLoadAsync() { ... }

// После — вариант 2: стандартный ValkarnTask + .Forget() на месте вызова
async ValkarnTask StartLoadAsync() { ... }
// Вызывается как:
StartLoadAsync().Forget();

MIG008 — Обнаружен Awaitable MainThreadAsync()

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Awaitable.MainThreadAsync() использовался в API Unity Awaitable для возврата в основной поток. Valkarn Tasks по умолчанию запускает продолжения в основном потоке через интеграцию с PlayerLoop, поэтому явные вызовы MainThreadAsync() обычно излишни и могут быть удалены.


MIG009 — Обнаружен UniTask.RunOnThreadPool

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

Замените на ValkarnTask.RunOnThreadPool. API идентичен.

// До
await UniTask.RunOnThreadPool(() => HeavyWork());

// После
await ValkarnTask.RunOnThreadPool(() => HeavyWork());

MIG010 — Обнаружен .ToCoroutine()

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

.ToCoroutine() был мостом UniTask для устаревших вызывающих на корутинах. Перепишите потребляющий код как метод async ValkarnTask вместо этого.

// До
IEnumerator LegacyCaller() { yield return MyUniTask().ToCoroutine(); }

// После
async ValkarnTask ModernCaller() { await MyValkarnTask(); }

MIG011 — Обнаружен UniTask.Create()

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

UniTask.Create(Func<UniTask>) оборачивает делегат-фабрику. Замените паттерном ValkarnTask.Promise<T> для управления завершением вручную.

// До
var task = UniTask.Create(async () => { await DoWork(); return 42; });

// После
var promise = new ValkarnTaskCompletionSource<int>();
DoWorkThenComplete(promise);
var task = promise.Task;

MIG012 — Обнаружен UniTask.Lazy/Defer

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

UniTask.Lazy<T> и UniTask.Defer существовали для избежания аллокаций, когда задача могла завершиться синхронно. Valkarn Tasks имеет встроенный быстрый путь без аллокаций для синхронного случая: возврат ValkarnTask.CompletedTask или ValkarnTask.FromResult(value) никогда не выделяет память. Удалите обёртки Lazy/Defer.


MIG013 — Обнаружен .ToUniTask()/.AsUniTask()

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

Вызовы преобразования из Unity Awaitable в UniTask. Удалите их; Valkarn Tasks нативно поддерживает мост для Awaitable (см. TT015).


MIG014 — Обнаружен UniTaskAsyncEnumerable

СвойствоЗначение
СерьёзностьWarning
КатегорияValkarnTaskMigration

UniTask поставлялся с собственными IUniTaskAsyncEnumerable<T> и утилитами UniTaskAsyncEnumerable. Используйте вместо них IAsyncEnumerable<T> из BCL с System.Linq.Async. IAsyncEnumerable<T> нативно поддерживается C# через await foreach.

// До
IUniTaskAsyncEnumerable<int> GetItems() { ... }

// После
IAsyncEnumerable<int> GetItems() { ... }

MIG015 — Обнаружен TimeoutController

СвойствоЗначение
СерьёзностьInfo
КатегорияValkarnTaskMigration

TimeoutController из UniTask был вспомогательным классом для повторно используемых таймаутов. Замените стандартным CancellationTokenSource, созданным с TimeSpan, что BCL поддерживает напрямую.

// До
var controller = new TimeoutController();
var ct = controller.Timeout(TimeSpan.FromSeconds(5));

// После
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var ct = cts.Token;

Подавление правил

Стандартные механизмы подавления Roslyn работают для всех правил:

// Подавить одно вхождение встроенно
#pragma warning disable TT012
while (true) { DoSomething(); }
#pragma warning restore TT012

Или через .editorconfig для подавления на уровне проекта:

[*.cs]
dotnet_diagnostic.TT012.severity = none

Правила миграции (MIG*) могут быть подавлены тем же способом, или отключены глобально после завершения миграции, установив серьёзность в none в .editorconfig.