Zum Hauptinhalt springen

Analyzer-Regeln

Valkarn Tasks wird mit zwei Roslyn-Analyzer-Paketen geliefert, die beim Importieren des Pakets automatisch aktiviert werden:

  • UnaPartidaMas.Valkarn.Tasks.SourceGen.dll — Regeln spezifisch für die Korrektheit von ValkarnTask und den Unity-Lebenszyklus. Regel-IDs beginnen mit TT.
  • UnaPartidaMas.Valkarn.Tasks.Analyzer.dll — Migrationsregeln für Codebasen, die von UniTask wechseln. Regel-IDs beginnen mit MIG. Diese Regeln werden nur ausgelöst, wenn Cysharp.Threading.Tasks.UniTask in der Kompilierung vorhanden ist, sodass sie in neuen Projekten still sind.

Beide Pakete sind vorkompilierte DLLs, die sich im Analyzers/-Ordner des Pakets befinden. Unity lädt sie als Roslyn-Analyzer über die .asmdef-Referenz; das _TestRunner~-Projekt lädt sie über <Analyzer>-Einträge in TestRunner.csproj.


Korrektheitsregeln (TT)

Diese Regeln erkennen Fehler im Zusammenhang mit der Einzelverbrauchsnatur von ValkarnTask und dem Missbrauch von Fire-and-Forget.


TT001 — ValkarnTask bereits abgewartet

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

ValkarnTask ist ein Einzelverbrauch: einmal abgewartet, wird das interne Token veraltet. Ein zweites await auf derselben Variable trifft auf dieses veraltete Token und wirft eine Ausnahme. Der Analyzer erkennt, wenn dieselbe lokale Variable, derselbe Parameter oder dasselbe Feld vom Typ ValkarnTask/ValkarnTask<T> mehr als einmal innerhalb derselben Methode abgewartet wird.

Ausgelöst bei:

async ValkarnTask Bad()
{
ValkarnTask work = DoWorkAsync();
await work; // erstes await — korrekt
await work; // TT001: bereits abgewartet
}

Behebung: Wenn Sie das Ergebnis verzweigen müssen, erfassen Sie es mit .AsResult() vor dem ersten await, oder strukturieren Sie den Code so um, dass die Task genau einmal abgewartet wird.

async ValkarnTask Good()
{
var result = await DoWorkAsync().AsResult();
// result in beiden Verzweigungen verwenden
}

TT002 — ValkarnTask nicht abgewartet oder verworfen

EigenschaftWert
SchwereFehler
KategorieValkarnTask
Code-FixNein

Ein Methodenaufruf, der ValkarnTask zurückgibt und als Ausdrucksanweisung verwendet wird — nicht abgewartet, nicht zugewiesen und nicht von .Forget() gefolgt — ist ein stiller Fehler. Im Innern der Task geworfene Ausnahmen werden niemals beobachtet, und die gepoolte Zustandsmaschine der Task wird niemals in den Pool zurückgegeben.

Der Analyzer prüft Ausdrucksanweisungen, bei denen der Ausdruckstyp zu ValkarnTask oder ValkarnTask<T> aufgelöst wird. Er überspringt:

  • Zuweisungsausdrücke (tasks[i] = DoWork())
  • Ketten, die mit .Forget() enden (beabsichtigtes Fire-and-Forget)

Ausgelöst bei:

void Bad()
{
LoadDataAsync(); // TT002: nicht abgewartet oder verworfen
ProcessItemAsync(); // TT002
}

Behebung — abwarten:

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

Behebung — explizites Fire-and-Forget:

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

TT013 — ValkarnTask zurückgegeben, aber nie verbraucht

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

Diese Regel-ID ist für zukünftige Datenflusanalyse reserviert. Sie soll das Muster „zugewiesen, aber nie abgewartet" erkennen — eine ValkarnTask in eine Variable speichern und dann nie abwarten oder verwerfen — was TT002 nicht abdeckt, da TT002 nur nackte Ausdrucksanweisungen untersucht.

Die aktuelle Implementierung registriert keine Syntaxaktionen. Wenn die Datenflussanalyse implementiert ist, wird TT013 TT002 ergänzen und folgendes abdecken:

async ValkarnTask Bad()
{
ValkarnTask task = DoWorkAsync(); // zugewiesen, aber nie abgewartet
DoOtherThing();
// task wird aufgegeben — TT013 (zukünftig)
}

Mit [FireAndForget] dekorierte Methoden werden ausgenommen.


Lebenszyklusregeln (TT)

Diese Regeln betreffen das Lebensdauermanagement von MonoBehaviour und Abbruchlogik.


TT010 — Auto-Abbruch aktiv für MonoBehaviour-Async-Methode

EigenschaftWert
SchwereInfo
KategorieValkarnTask
Code-FixNein

Ein informativer Hinweis. Jede async ValkarnTask- (oder async ValkarnTask<T>-) Methode innerhalb einer Klasse, die von UnityEngine.MonoBehaviour erbt, wird automatisch abgebrochen, wenn das Objekt zerstört wird — es sei denn, die Methode ist mit [NoAutoCancel] dekoriert. Diese Diagnose macht dieses Verhalten in der IDE sichtbar, ohne dass sich der Entwickler daran erinnern muss.

Ausgelöst bei:

public class MyBehaviour : MonoBehaviour
{
async ValkarnTask LoadLevel() // TT010: Auto-Abbruch aktiv
{
await ValkarnTask.Delay(2000);
}
}

Zum Deaktivieren (und Unterdrücken von TT010 für diese Methode) fügen Sie [NoAutoCancel] hinzu:

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

TT011 — WhenAll mischt verschiedene Lebensdauerbereiche

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

ValkarnTask.WhenAll, aufgerufen mit Tasks aus verschiedenen Lebensdauerbereichen, kann überraschendes Teilabbruch-Verhalten erzeugen. Wenn eine Task an ein MonoBehaviour gebunden ist (zurückgegeben von einer Instanzmethode einer Klasse, die MonoBehaviour erbt) und eine andere ungebunden ist (eine statische Task oder aus einer Nicht-MonoBehaviour-Klasse), wird das Zerstören des Objekts eine Task abbrechen, aber nicht die andere, und den Kombinator in einem unbestimmten Zustand hinterlassen.

Ausgelöst bei:

// Annahme: EnemyAI : MonoBehaviour
await ValkarnTask.WhenAll(
enemyAI.PatrolAsync(), // gebunden: auto-abgebrochen bei Destroy
GlobalMusic.FadeAsync() // ungebunden: lebt unbegrenzt
);
// TT011: WhenAll mischt Lebensdauern — PatrolAsync() vs GlobalMusic.FadeAsync()

Behebung — beiden Tasks ein gemeinsames Abbruch-Token geben:

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

TT012 — Async-Schleife ohne Abbruchprüfung

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

Eine async ValkarnTask-Methode, die eine for-, while-, foreach- oder do-while-Schleife enthält, deren Körper keine Abbruchprüfung hat, ist eine „Zombie-Schleife": Wenn ein Lebenszyklusabbruch ausgelöst wird (z. B. ein MonoBehaviour wird zerstört), kann das Auto-Abbruch-Signal die Schleife nicht unterbrechen. Die Schleife läuft weiter, obwohl das besitzende Objekt verschwunden ist.

Der Analyzer betrachtet einen Schleifenkörper als abbruchgeprüft, wenn er eines der folgenden enthält:

  • Einen await-Ausdruck (die abgewartete Operation kann das Token beobachten und OperationCanceledException werfen)
  • ThrowIfCancellationRequested (als Bezeichner oder Member-Zugriff)
  • IsCancellationRequested (als Bezeichner oder Member-Zugriff)

Die Prüfung steigt nicht in verschachtelte Lambdas oder lokale Funktionen ab.

Ausgelöst bei:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true) // TT012: keine Abbruchprüfung im Körper
{
ProcessNextItem();
Thread.Sleep(16); // synchron — kein await
}
}

Behebung — ein await oder eine explizite Prüfung hinzufügen:

async ValkarnTask PollForever(CancellationToken ct)
{
while (true)
{
ct.ThrowIfCancellationRequested();
ProcessNextItem();
await ValkarnTask.Yield(); // erfüllt ebenfalls die Prüfung
}
}

TT014 — [NoAutoCancel] ohne CancellationToken-Parameter

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

[NoAutoCancel] auf einer async ValkarnTask-Methode in einem MonoBehaviour deaktiviert den automatischen Lebenszyklusabbruch. Wenn die Methode jedoch keinen CancellationToken-Parameter hat, hat sie keinen Mechanismus, um Abbrüche überhaupt zu beobachten, was das Attribut sinnlos macht und fast sicher einen vergessenen Parameter anzeigt.

Ausgelöst bei:

public class Enemy : MonoBehaviour
{
[NoAutoCancel]
async ValkarnTask Chase() // TT014: [NoAutoCancel] ohne CancellationToken-Parameter
{
await ValkarnTask.Delay(1000);
}
}

Behebung — einen CancellationToken-Parameter hinzufügen:

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

Informationsregeln (TT)


TT015 — Awaitable-Bridge-Adapter generiert

EigenschaftWert
SchwereInfo
KategorieValkarnTask
Code-FixNein

Wenn Sie ein Unity-Awaitable (aus UnityEngine) innerhalb einer async ValkarnTask-Methode abwarten, generiert Valkarn Tasks automatisch einen Bridge-Adapter über AwaitableBridge. Diese Diagnose ist informativ: Sie bestätigt, dass die Bridge aktiv ist. Es ist keine Aktion erforderlich.

Ausgelöst bei:

async ValkarnTask LoadScene()
{
await SceneManager.LoadSceneAsync("Main"); // TT015: Bridge-Adapter generiert
}

Es ist keine Änderung nötig. Die Bridge übernimmt die Konvertierung transparent.


Code-Qualitätsregeln (TT)


TT016 — Async-Methode ohne await

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

Eine async ValkarnTask- (oder async ValkarnTask<T>-) Methode, die keine await-Ausdrücke enthält — einschließlich await foreach, await using und await in using-Deklarationen — verursacht den vollständigen Overhead der Zustandsmaschinen-Allokation ohne Nutzen. Der Compiler generiert trotzdem eine Zustandsmaschine, aber da es keinen Suspendierungspunkt gibt, wird die Methode immer synchron abgeschlossen.

Der Analyzer prüft auf: AwaitExpressionSyntax, foreach mit await-Schlüsselwort, using-Deklarationen mit await-Schlüsselwort und using-Anweisungen mit await-Schlüsselwort.

Ausgelöst bei:

async ValkarnTask<int> ComputeTotal()  // TT016: kein await im Körper
{
return items.Sum(x => x.Value);
}

Behebung — async entfernen und eine abgeschlossene Task zurückgeben:

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

Oder für eine void-Rückgabe:

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

TT017 — [FireAndForget] auf ValkarnTask<T>

EigenschaftWert
SchwereWarnung
KategorieValkarnTask
Code-FixNein

[FireAndForget] signalisiert, dass eine Methode absichtlich ohne Abwarten ihres Ergebnisses ausgeführt wird. Das Anwenden auf eine Methode, die ValkarnTask<T> (typisiertes Ergebnis) zurückgibt, verwirft immer T, was die typisierte Rückgabe bedeutungslos macht. Die Methode sollte stattdessen ValkarnTask (void) zurückgeben.

Ausgelöst bei:

[FireAndForget]
async ValkarnTask<int> SendReport() // TT017: Rückgabewert wird verworfen
{
await UploadAsync();
return 42; // diese 42 wird niemals gesehen
}

Behebung — den Rückgabetyp zu ValkarnTask ändern:

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

Migrationsregeln (MIG)

Der Migrations-Analyzer wird nur aktiviert, wenn der Typ Cysharp.Threading.Tasks.UniTask in der Kompilierung vorhanden ist. Die Regeln sind informativ oder Warnungen, um den Übergang von UniTask zu Valkarn Tasks zu leiten. Keine dieser Regeln hat Auto-Fixes; die nachstehenden Beschreibungen erklären die manuelle Änderung.


MIG001 — UniTask-Typ erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

Eine Verwendung eines UniTask-Typs (das Struct selbst oder UniTask<T>) wurde erkannt. Ersetzen Sie ihn durch das ValkarnTask- oder ValkarnTask<T>-Äquivalent.

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

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

MIG002 — cancelImmediately-Parameter ist unnötig

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

UniTasks Delay und andere zeitbasierte Methoden akzeptieren einen cancelImmediately-Parameter für schnellen Abbruch. Valkarn Tasks bricht standardmäßig sofort ab — es gibt keinen cancelImmediately-Parameter. Entfernen Sie das Argument.

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

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

MIG003 — SuppressCancellationThrow() erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

UniTasks .SuppressCancellationThrow() konvertiert eine abgebrochene Task in ein (bool isCancelled, T result)-Tupel ohne zu werfen. Valkarn Tasks verwendet .AsResult() für denselben Zweck, das ein Result<T>-Struct zurückgibt.

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

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

MIG004 — Awaitable-Rückgabetyp erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

Eine Methode gibt Unitys Awaitable-Typ zurück. Erwägen Sie, ihn durch ValkarnTask zu ersetzen, das nativ mit der Valkarn Tasks-Laufzeit integriert und Auto-Abbruch, Pooling und den vollständigen Kombinator-Satz unterstützt.


MIG005 — SingleConsumerUnbounded-Channel erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

Channel.CreateSingleConsumerUnbounded<T>() von UniTask erstellt einen Channel ohne Gegendruck. Erwägen Sie, ihn durch ValkarnTask.Channel.CreateBounded<T>(capacity) zu ersetzen, um Gegendruck hinzuzufügen und unbegrenztes Speicherwachstum unter Last zu verhindern.

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

// Nachher (mit Gegendruck)
var ch = ValkarnTask.Channel.CreateBounded<Event>(capacity: 256);

// Oder wenn unbegrenzt beabsichtigt ist
var ch = ValkarnTask.Channel.CreateUnbounded<Event>();

MIG006 — UniTask PlayerLoopTiming erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

Ein PlayerLoopTiming-Enum-Wert aus dem Cysharp.Threading.Tasks-Namespace wurde erkannt. Ändern Sie die using-Direktive zu UnaPartidaMas.Valkarn.Tasks; die Enum-Wertnamen sind identisch.

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

// Nachher
using UnaPartidaMas.Valkarn.Tasks;
timing = PlayerLoopTiming.Update; // gleicher Name, anderer Namespace

MIG007 — async UniTaskVoid erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

async UniTaskVoid war UniTasks Fire-and-Forget-Methodenmuster. Valkarn Tasks ersetzt es durch zwei Optionen:

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

// Nachher — Option 1: [FireAndForget]-Attribut
[FireAndForget]
async ValkarnTask StartLoadAsync() { ... }

// Nachher — Option 2: Standard-ValkarnTask + .Forget() an der Aufrufstelle
async ValkarnTask StartLoadAsync() { ... }
// Aufgerufen als:
StartLoadAsync().Forget();

MIG008 — Awaitable MainThreadAsync() erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

Awaitable.MainThreadAsync() wurde in Unitys Awaitable-API verwendet, um zum Haupt-Thread zurückzuwechseln. Valkarn Tasks führt Fortsetzungen standardmäßig auf dem Haupt-Thread via PlayerLoop-Integration aus, sodass explizite MainThreadAsync()-Aufrufe meist unnötig sind und entfernt werden können.


MIG009 — UniTask.RunOnThreadPool erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

Ersetzen Sie durch ValkarnTask.RunOnThreadPool. Die API ist identisch.

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

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

MIG010 — .ToCoroutine() erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

.ToCoroutine() war eine UniTask-Bridge für Legacy-Coroutine-Aufrufer. Schreiben Sie den verbrauchenden Code stattdessen als async ValkarnTask-Methode um.

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

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

MIG011 — UniTask.Create() erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

UniTask.Create(Func<UniTask>) umhüllt ein Factory-Delegate. Ersetzen Sie es durch das ValkarnTask.Promise<T>-Muster für manuell gesteuerten Abschluss.

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

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

MIG012 — UniTask.Lazy/Defer erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

UniTask.Lazy<T> und UniTask.Defer existierten, um Allokationen zu vermeiden, wenn eine Task synchron abschließen könnte. Valkarn Tasks hat einen allokationsfreien synchronen Schnellpfad eingebaut: ValkarnTask.CompletedTask oder ValkarnTask.FromResult(value) zurückzugeben alloziert nie. Entfernen Sie die Lazy/Defer-Wrapper.


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

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

Konvertierungsaufrufe von Unity Awaitable zu UniTask. Entfernen Sie sie; Valkarn Tasks überbrückt Awaitable nativ (siehe TT015).


MIG014 — UniTaskAsyncEnumerable erkannt

EigenschaftWert
SchwereWarnung
KategorieValkarnTaskMigration

UniTask lieferte sein eigenes IUniTaskAsyncEnumerable<T> und UniTaskAsyncEnumerable-Utilities. Verwenden Sie stattdessen IAsyncEnumerable<T> aus der BCL mit System.Linq.Async. IAsyncEnumerable<T> wird nativ von C# await foreach unterstützt.

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

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

MIG015 — TimeoutController erkannt

EigenschaftWert
SchwereInfo
KategorieValkarnTaskMigration

UniTasks TimeoutController war ein Helfer für wiederverwendbare Timeouts. Ersetzen Sie ihn durch eine Standard-CancellationTokenSource, die mit einem TimeSpan konstruiert wird, was die BCL direkt unterstützt.

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

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

Regeln unterdrücken

Standard-Roslyn-Unterdrückungsmechanismen funktionieren für alle Regeln:

// Eine einzelne Vorkommen inline unterdrücken
#pragma warning disable TT012
while (true) { DoSomething(); }
#pragma warning restore TT012

Oder über .editorconfig für projektweite Unterdrückung:

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

Migrationsregeln (MIG*) können auf die gleiche Weise unterdrückt werden oder global deaktiviert werden, sobald die Migration abgeschlossen ist, indem die Schwere in .editorconfig auf none gesetzt wird.