Pular para o conteúdo principal

Regras do Analisador

O Valkarn Tasks inclui dois pacotes de analisador Roslyn que se ativam automaticamente quando o pacote é importado:

  • UnaPartidaMas.Valkarn.Tasks.SourceGen.dll — Regras específicas à correção do ValkarnTask e ao ciclo de vida do Unity. Os IDs de regra começam com TT.
  • UnaPartidaMas.Valkarn.Tasks.Analyzer.dll — Regras de migração para bases de código que estão migrando do UniTask. Os IDs de regra começam com MIG. Essas regras disparam apenas quando Cysharp.Threading.Tasks.UniTask está presente na compilação, portanto são silenciosas em novos projetos.

Ambos os pacotes são DLLs pré-compiladas localizadas na pasta Analyzers/ do pacote. O Unity os carrega como analisadores Roslyn via a referência .asmdef; o projeto _TestRunner~ os carrega via itens <Analyzer> em TestRunner.csproj.


Regras de Correção (TT)

Essas regras detectam bugs relacionados à natureza de consumo único do ValkarnTask e uso indevido de fire-and-forget.


TT001 — ValkarnTask já aguardado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

ValkarnTask é de consumo único: uma vez aguardado, o token interno fica obsoleto. Um segundo await na mesma variável encontrará esse token obsoleto e lançará exceção. O analisador detecta quando a mesma variável local, parâmetro ou campo do tipo ValkarnTask/ValkarnTask<T> é aguardado mais de uma vez dentro do mesmo método.

Dispara em:

async ValkarnTask Bad()
{
ValkarnTask work = DoWorkAsync();
await work; // primeiro await — ok
await work; // TT001: já aguardado
}

Correção: Se precisar ramificar no resultado, capture-o com .AsResult() antes do primeiro await, ou reestruture para que a task seja aguardada exatamente uma vez.

async ValkarnTask Good()
{
var result = await DoWorkAsync().AsResult();
// usar resultado em ambas as ramificações
}

TT002 — ValkarnTask não aguardado nem descartado

PropriedadeValor
SeveridadeErro
CategoriaValkarnTask
Correção de códigoNão

Uma chamada de método que retorna ValkarnTask usada como declaração de expressão — não aguardada, não atribuída e não seguida de .Forget() — é um bug silencioso. Exceções lançadas dentro da task nunca são observadas, e a máquina de estados em pool da task nunca é retornada ao pool.

O analisador verifica declarações de expressão onde o tipo da expressão resolve para ValkarnTask ou ValkarnTask<T>. Ele ignora:

  • Expressões de atribuição (tasks[i] = DoWork())
  • Cadeias terminando em .Forget() (fire-and-forget intencional)

Dispara em:

void Bad()
{
LoadDataAsync(); // TT002: não aguardado nem descartado
ProcessItemAsync(); // TT002
}

Correção — aguardá-la:

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

Correção — fire-and-forget explícito:

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

TT013 — ValkarnTask retornado mas nunca consumido

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

Este ID de regra é reservado para análise de fluxo de dados futura. Pretende detectar o padrão atribuído-mas-nunca-aguardado — armazenar um ValkarnTask em uma variável e então nunca aguardá-lo ou descartá-lo — que o TT002 não cobre porque o TT002 só examina declarações de expressão puras.

A implementação atual não registra nenhuma ação de sintaxe. Quando a análise de fluxo de dados for implementada, o TT013 complementará o TT002 cobrindo:

async ValkarnTask Bad()
{
ValkarnTask task = DoWorkAsync(); // atribuído mas nunca aguardado
DoOtherThing();
// task é abandonada — TT013 (futuro)
}

Métodos decorados com [FireAndForget] serão isentos.


Regras de Ciclo de Vida (TT)

Essas regras se relacionam ao gerenciamento de tempo de vida do MonoBehaviour e cancelamento.


TT010 — Auto-cancel ativo para método async do MonoBehaviour

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTask
Correção de códigoNão

Uma dica informacional. Qualquer método async ValkarnTask (ou async ValkarnTask<T>) dentro de uma classe que herda de UnityEngine.MonoBehaviour será automaticamente cancelado quando o objeto for destruído, a menos que o método seja decorado com [NoAutoCancel]. Este diagnóstico torna esse comportamento visível no IDE sem exigir que o desenvolvedor se lembre disso.

Dispara em:

public class MyBehaviour : MonoBehaviour
{
async ValkarnTask LoadLevel() // TT010: auto-cancel ativo
{
await ValkarnTask.Delay(2000);
}
}

Para desativar (e suprimir TT010 para aquele método), adicione [NoAutoCancel]:

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

TT011 — WhenAll mistura escopos de lifetime diferentes

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

ValkarnTask.WhenAll chamado com tasks de diferentes escopos de lifetime pode produzir comportamento surpresa de cancelamento parcial. Se uma task está vinculada a um MonoBehaviour (retornada por um método de instância de uma classe que herda MonoBehaviour) e outra não está vinculada (uma task estática, ou de uma classe não-MonoBehaviour), destruir o objeto irá cancelar uma task mas não a outra, deixando o combinador em um estado indeterminado.

Dispara em:

// Assumindo EnemyAI : MonoBehaviour
await ValkarnTask.WhenAll(
enemyAI.PatrolAsync(), // vinculado: cancelado automaticamente ao Destroy
GlobalMusic.FadeAsync() // não vinculado: vive indefinidamente
);
// TT011: WhenAll mistura lifetimes — PatrolAsync() vs GlobalMusic.FadeAsync()

Correção — dar a ambas as tasks um token de cancelamento compartilhado:

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

TT012 — Loop async sem verificação de cancelamento

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

Um método async ValkarnTask contendo um loop for, while, foreach ou do-while cujo corpo não tem verificação de cancelamento é um "loop zumbi": se o cancelamento de ciclo de vida disparar (ex.: um MonoBehaviour é destruído), o sinal de auto-cancel não pode quebrar o loop. O loop continua executando mesmo que o objeto proprietário tenha ido embora.

O analisador considera que o corpo de um loop tem uma verificação de cancelamento se contiver qualquer um dos seguintes:

  • Uma expressão await (a operação aguardada pode observar o token e lançar OperationCanceledException)
  • ThrowIfCancellationRequested (como identificador ou acesso a membro)
  • IsCancellationRequested (como identificador ou acesso a membro)

A verificação não desce para lambdas aninhadas ou funções locais.

Dispara em:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true) // TT012: sem verificação de cancelamento no corpo
{
ProcessNextItem();
Thread.Sleep(16); // síncrono — não é um await
}
}

Correção — adicionar um await ou uma verificação explícita:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true)
{
ct.ThrowIfCancellationRequested();
ProcessNextItem();
await ValkarnTask.Yield(); // também satisfaz a verificação
}
}

TT014 — [NoAutoCancel] sem parâmetro CancellationToken

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

[NoAutoCancel] em um método async ValkarnTask em um MonoBehaviour opta o método para fora do cancelamento automático de ciclo de vida. Mas se o método não tem parâmetro CancellationToken, não tem mecanismo para observar o cancelamento, tornando o atributo inútil e quase certamente indicando um parâmetro esquecido.

Dispara em:

public class Enemy : MonoBehaviour
{
[NoAutoCancel]
async ValkarnTask Chase() // TT014: [NoAutoCancel] mas sem parâmetro CancellationToken
{
await ValkarnTask.Delay(1000);
}
}

Correção — adicionar um parâmetro CancellationToken:

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

Regras Informacionais (TT)


TT015 — Adaptador bridge do Awaitable gerado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTask
Correção de códigoNão

Quando você faz await em um Awaitable do Unity (de UnityEngine) dentro de um método async ValkarnTask, o Valkarn Tasks gera automaticamente um adaptador bridge via AwaitableBridge. Este diagnóstico é informacional: confirma que a bridge está em vigor. Nenhuma ação é necessária.

Dispara em:

async ValkarnTask LoadScene()
{
await SceneManager.LoadSceneAsync("Main"); // TT015: adaptador bridge gerado
}

Nenhuma alteração é necessária. A bridge lida com a conversão de forma transparente.


Regras de Qualidade de Código (TT)


TT016 — Método async sem await

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

Um método async ValkarnTask (ou async ValkarnTask<T>) que não contém expressões await — incluindo await foreach, await using e await dentro de declarações using — incorre no overhead completo de alocação de máquina de estados sem benefício. O compilador ainda gera uma máquina de estados, mas como não há ponto de suspensão, o método sempre completa de forma síncrona.

O analisador verifica: AwaitExpressionSyntax, foreach com palavra-chave await, declarações using com palavra-chave await e declarações using com palavra-chave await.

Dispara em:

async ValkarnTask<int> ComputeTotal()  // TT016: sem await no corpo
{
return items.Sum(x => x.Value);
}

Correção — remover async e retornar uma task concluída:

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

Ou para retorno void:

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

TT017 — [FireAndForget] em ValkarnTask<T>

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTask
Correção de códigoNão

[FireAndForget] sinaliza que um método é intencionalmente executado sem aguardar seu resultado. Aplicá-lo a um método que retorna ValkarnTask<T> (resultado tipado) sempre descarta T, tornando o retorno tipado sem sentido. O método deve retornar ValkarnTask (void) em vez disso.

Dispara em:

[FireAndForget]
async ValkarnTask<int> SendReport() // TT017: valor de retorno é descartado
{
await UploadAsync();
return 42; // este 42 nunca é visto
}

Correção — alterar o tipo de retorno para ValkarnTask:

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

Regras de Migração (MIG)

O analisador de migração se ativa apenas quando o tipo Cysharp.Threading.Tasks.UniTask está presente na compilação. As regras são informacionais ou avisos para guiar a transição do UniTask para o Valkarn Tasks. Nenhuma dessas regras tem correção automática; as descrições abaixo explicam a alteração manual.


MIG001 — Tipo UniTask detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Um uso de um tipo UniTask (a struct em si ou UniTask<T>) foi detectado. Substitua pelo equivalente ValkarnTask ou ValkarnTask<T>.

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

// Depois
ValkarnTask<Sprite> LoadSprite(string path) { ... }

MIG002 — Parâmetro cancelImmediately é desnecessário

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Os métodos Delay e outros baseados em tempo do UniTask aceitam um parâmetro cancelImmediately para optar por cancelamento rápido. O Valkarn Tasks cancela imediatamente por padrão — não há parâmetro cancelImmediately. Remova o argumento.

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

// Depois
await ValkarnTask.Delay(1000, ct);

MIG003 — SuppressCancellationThrow() detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

.SuppressCancellationThrow() do UniTask converte uma task cancelada em uma tupla (bool isCancelled, T result) sem lançar. O Valkarn Tasks usa .AsResult() para o mesmo propósito, que retorna uma struct Result<T>.

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

// Depois
var result = await myValkarnTask.AsResult();
if (result.IsCanceled) { ... }
T value = result.Value;

MIG004 — Tipo de retorno Awaitable detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Um método retorna o tipo Awaitable do Unity. Considere substituí-lo por ValkarnTask, que se integra nativamente com o runtime do Valkarn Tasks e suporta cancelamento automático, pooling e o conjunto completo de combinadores.


MIG005 — Canal SingleConsumerUnbounded detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

Channel.CreateSingleConsumerUnbounded<T>() do UniTask cria um canal sem backpressure. Considere substituí-lo por ValkarnTask.Channel.CreateBounded<T>(capacity) para adicionar backpressure e evitar crescimento ilimitado de memória sob carga.

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

// Depois (com backpressure)
var ch = ValkarnTask.Channel.CreateBounded<Event>(capacity: 256);

// Ou se ilimitado é intencional
var ch = ValkarnTask.Channel.CreateUnbounded<Event>();

MIG006 — PlayerLoopTiming do UniTask detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Um valor de enum PlayerLoopTiming do namespace Cysharp.Threading.Tasks foi detectado. Altere a diretiva using para UnaPartidaMas.Valkarn.Tasks; os nomes de valor do enum são idênticos.

// Antes
using Cysharp.Threading.Tasks;
timing = PlayerLoopTiming.Update;

// Depois
using UnaPartidaMas.Valkarn.Tasks;
timing = PlayerLoopTiming.Update; // mesmo nome, namespace diferente

MIG007 — async UniTaskVoid detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

async UniTaskVoid era o padrão de método fire-and-forget do UniTask. O Valkarn Tasks substitui com duas opções:

// Antes
async UniTaskVoid StartLoadAsync() { ... }

// Depois — opção 1: atributo [FireAndForget]
[FireAndForget]
async ValkarnTask StartLoadAsync() { ... }

// Depois — opção 2: ValkarnTask padrão + .Forget() no local de chamada
async ValkarnTask StartLoadAsync() { ... }
// Chamado como:
StartLoadAsync().Forget();

MIG008 — Awaitable MainThreadAsync() detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Awaitable.MainThreadAsync() era usado na API Awaitable do Unity para voltar para a thread principal. O Valkarn Tasks executa continuações na thread principal por padrão via integração com o PlayerLoop, portanto chamadas explícitas a MainThreadAsync() geralmente são desnecessárias e podem ser removidas.


MIG009 — UniTask.RunOnThreadPool detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

Substitua por ValkarnTask.RunOnThreadPool. A API é a mesma.

// Antes
await UniTask.RunOnThreadPool(() => HeavyWork());

// Depois
await ValkarnTask.RunOnThreadPool(() => HeavyWork());

MIG010 — .ToCoroutine() detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

.ToCoroutine() era uma bridge UniTask para chamadores de coroutine legados. Reescreva o código consumidor como um método async ValkarnTask em vez disso.

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

// Depois
async ValkarnTask ModernCaller() { await MyValkarnTask(); }

MIG011 — UniTask.Create() detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

UniTask.Create(Func<UniTask>) envolve um delegate factory. Substitua pelo padrão ValkarnTask.Promise<T> para conclusão controlada manualmente.

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

// Depois
var promise = new ValkarnTaskCompletionSource<int>();
DoWorkThenComplete(promise);
var task = promise.Task;

MIG012 — UniTask.Lazy/Defer detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

UniTask.Lazy<T> e UniTask.Defer existiam para evitar alocação quando uma task poderia completar de forma síncrona. O Valkarn Tasks tem um caminho rápido síncrono de zero alocação integrado: retornar ValkarnTask.CompletedTask ou ValkarnTask.FromResult(value) nunca aloca. Remova os wrappers Lazy/Defer.


MIG013 — .ToUniTask()/.AsUniTask() detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

Chamadas de conversão do Awaitable do Unity para UniTask. Remova-as; o Valkarn Tasks faz bridge com Awaitable nativamente (veja TT015).


MIG014 — UniTaskAsyncEnumerable detectado

PropriedadeValor
SeveridadeAviso
CategoriaValkarnTaskMigration

O UniTask incluía seu próprio IUniTaskAsyncEnumerable<T> e utilitários UniTaskAsyncEnumerable. Use IAsyncEnumerable<T> da BCL com System.Linq.Async em vez disso. IAsyncEnumerable<T> é suportado nativamente pelo C# await foreach.

// Antes
IUniTaskAsyncEnumerable<int> GetItems() { ... }

// Depois
IAsyncEnumerable<int> GetItems() { ... }

MIG015 — TimeoutController detectado

PropriedadeValor
SeveridadeInfo
CategoriaValkarnTaskMigration

O TimeoutController do UniTask era um helper para timeouts reutilizáveis. Substitua por um CancellationTokenSource padrão construído com um TimeSpan, que a BCL suporta diretamente.

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

// Depois
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var ct = cts.Token;

Suprimindo Regras

Os mecanismos de supressão padrão do Roslyn funcionam para todas as regras:

// Suprimir uma única ocorrência inline
#pragma warning disable TT012
while (true) { DoSomething(); }
#pragma warning restore TT012

Ou via .editorconfig para suprimir em todo o projeto:

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

As regras de migração (MIG*) podem ser suprimidas da mesma forma, ou desabilitadas globalmente uma vez que a migração esteja completa definindo a severidade como none em .editorconfig.