إنتقل إلى المحتوى الرئيسي

قواعد المحلِّل

تشحن 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 جرى انتظاره بالفعل

الخاصيةالقيمة
الخطورةتحذير
الفئةValkarnTask
إصلاح كودلا

ValkarnTask أحادي الاستهلاك: بمجرد الانتظار، يصبح الرمز الداخلي قديمًا. await ثانٍ على نفس المتغير سيواجه هذا الرمز القديم ويُطرح. يكتشف المحلِّل متى يُنتظَر نفس المتغير المحلي أو المعامل أو الحقل من النوع ValkarnTask/ValkarnTask<T> أكثر من مرة داخل نفس الدالة.

يُطلق على:

async ValkarnTask Bad()
{
ValkarnTask work = DoWorkAsync();
await work; // الانتظار الأول — صحيح
await work; // TT001: جرى انتظاره بالفعل
}

الإصلاح: إذا احتجت للتفرع بناءً على النتيجة، التقطها بـ.AsResult() قبل الانتظار الأول، أو أعد الهيكلة بحيث يُنتظَر Task مرة واحدة بالضبط.

async ValkarnTask Good()
{
var result = await DoWorkAsync().AsResult();
// استخدم result في كلا الفرعين
}

TT002 — ValkarnTask لم يُنتظَر ولم يُتجاهَل

الخاصيةالقيمة
الخطورةخطأ
الفئةValkarnTask
إصلاح كودلا

استدعاء دالة تُرجع ValkarnTask مستخدَم كعبارة تعبير — لم يُنتظَر، ولم يُخصَّص، ولم يُتبَع بـ.Forget() — هو خطأ صامت. الاستثناءات المُطرحة داخل Task لا تُلاحَظ أبدًا، وآلة الحالة المُجمَّعة في التاسك لا تُعاد أبدًا إلى المجموعة.

يتحقق المحلِّل من عبارات التعبير حيث يُحلِّل نوع التعبير إلى 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 أُرجع لكن لم يُستهلَك

الخاصيةالقيمة
الخطورةتحذير
الفئةValkarnTask
إصلاح كودلا

معرّف هذه القاعدة محجوز لتحليل تدفق البيانات المستقبلي. يُقصد بها اكتشاف نمط الإسناد دون الانتظار — تخزين ValkarnTask في متغير ثم عدم انتظاره أو تجاهله أبدًا — وهو ما لا تغطيه TT002 لأن TT002 تفحص فقط عبارات التعبير المجردة.

التنفيذ الحالي لا يسجِّل أي إجراءات تركيبية. عند تنفيذ تحليل تدفق البيانات، ستُكمِّل TT013 TT002 بتغطية:

async ValkarnTask Bad()
{
ValkarnTask task = DoWorkAsync(); // مُخصَّص لكن لم يُنتظَر أبدًا
DoOtherThing();
// task متروكة — TT013 (مستقبلي)
}

الدوال المزيَّنة بـ[FireAndForget] ستكون معفاة.


قواعد دورة الحياة (TT)

هذه القواعد تتعلق بإدارة عمر MonoBehaviour والإلغاء.


TT010 — الإلغاء التلقائي نشط لدالة async في MonoBehaviour

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTask
إصلاح كودلا

تلميح إعلامي. أي دالة async ValkarnTask (أو async ValkarnTask<T>) داخل فئة ترث من UnityEngine.MonoBehaviour ستُلغى تلقائيًا عند تدمير الكائن، ما لم تُزيَّن الدالة بـ[NoAutoCancel]. يجعل هذا التشخيص هذا السلوك مرئيًا في بيئة التطوير المتكاملة دون الحاجة لتذكره.

يُطلق على:

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 يخلط نطاقات عمر مختلفة

الخاصيةالقيمة
الخطورةتحذير
الفئة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 بدون تحقق من الإلغاء

الخاصيةالقيمة
الخطورةتحذير
الفئة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

الخاصيةالقيمة
الخطورةتحذير
الفئة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 مُولَّد

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTask
إصلاح كودلا

عند انتظار Awaitable من Unity (من UnityEngine) داخل دالة async ValkarnTask، تُولِّد Valkarn Tasks محوِّل جسر تلقائيًا عبر AwaitableBridge. هذا التشخيص إعلامي: يؤكد أن الجسر مفعَّل. لا يلزم اتخاذ أي إجراء.

يُطلق على:

async ValkarnTask LoadScene()
{
await SceneManager.LoadSceneAsync("Main"); // TT015: محوِّل جسر مُولَّد
}

لا يلزم تغيير. يتعامل الجسر مع التحويل بشكل شفاف.


قواعد جودة الكود (TT)


TT016 — دالة async بدون await

الخاصيةالقيمة
الخطورةتحذير
الفئة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>

الخاصيةالقيمة
الخطورةتحذير
الفئة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 مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

اكتُشف استخدام نوع UniTask (البنية نفسها أو UniTask<T>). استبدله بما يعادله ValkarnTask أو ValkarnTask<T>.

// قبل
UniTask<Sprite> LoadSprite(string path) { ... }

// بعد
ValkarnTask<Sprite> LoadSprite(string path) { ... }

MIG002 — معامل cancelImmediately غير ضروري

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

تقبل دوال Delay وغيرها من الدوال المبنية على الوقت في UniTask معامل cancelImmediately للاشتراك في الإلغاء السريع. تُلغي Valkarn Tasks فورًا بشكل افتراضي — لا يوجد معامل cancelImmediately. أزل الوسيط.

// قبل
await UniTask.Delay(1000, cancelImmediately: true, cancellationToken: ct);

// بعد
await ValkarnTask.Delay(1000, ct);

MIG003 — SuppressCancellationThrow() مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئة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 مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

دالة تُرجع نوع Awaitable من Unity. فكِّر في استبداله بـValkarnTask، الذي يتكامل بشكل أصلي مع بيئة تشغيل Valkarn Tasks ويدعم الإلغاء التلقائي والتجميع ومجموعة المُدمِجات الكاملة.


MIG005 — قناة SingleConsumerUnbounded مُكتشَفة

الخاصيةالقيمة
الخطورةتحذير
الفئة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 — PlayerLoopTiming من UniTask مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئة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 مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئة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() مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

استُخدم Awaitable.MainThreadAsync() في واجهة برمجة Awaitable في Unity للعودة إلى الخيط الرئيسي. تُشغِّل Valkarn Tasks الاستمرارات على الخيط الرئيسي بشكل افتراضي عبر تكامل PlayerLoop، لذا فإن استدعاءات MainThreadAsync() الصريحة عادةً غير ضرورية ويمكن حذفها.


MIG009 — UniTask.RunOnThreadPool مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئةValkarnTaskMigration

استبدل بـValkarnTask.RunOnThreadPool. واجهة برمجة التطبيقات متطابقة.

// قبل
await UniTask.RunOnThreadPool(() => HeavyWork());

// بعد
await ValkarnTask.RunOnThreadPool(() => HeavyWork());

MIG010 — .ToCoroutine() مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئةValkarnTaskMigration

.ToCoroutine() كان جسر UniTask للمستدعين القديمين للكوروتين. أعد كتابة الكود المستهلِك كدالة async ValkarnTask بدلاً من ذلك.

// قبل
IEnumerator LegacyCaller() { yield return MyUniTask().ToCoroutine(); }

// بعد
async ValkarnTask ModernCaller() { await MyValkarnTask(); }

MIG011 — UniTask.Create() مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئة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 مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

كانت UniTask.Lazy<T> وUniTask.Defer موجودتَين لتجنب التخصيص عندما قد تكتمل مهمة بشكل متزامن. تمتلك Valkarn Tasks مسارًا سريعًا متزامن صفر التخصيص مدمجًا: إرجاع ValkarnTask.CompletedTask أو ValkarnTask.FromResult(value) لا يُخصِّص أبدًا. أزل أغلفة Lazy/Defer.


MIG013 — .ToUniTask()/.AsUniTask() مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئةValkarnTaskMigration

استدعاءات التحويل من Unity Awaitable إلى UniTask. أزلها؛ تجسِّر Valkarn Tasks Awaitable بشكل أصلي (انظر TT015).


MIG014 — UniTaskAsyncEnumerable مُكتشَف

الخاصيةالقيمة
الخطورةتحذير
الفئةValkarnTaskMigration

شحنت UniTask IUniTaskAsyncEnumerable<T> الخاصة بها وأدوات UniTaskAsyncEnumerable. استخدم IAsyncEnumerable<T> من BCL مع System.Linq.Async بدلاً من ذلك. IAsyncEnumerable<T> مدعوم بشكل أصلي من await foreach في C#.

// قبل
IUniTaskAsyncEnumerable<int> GetItems() { ... }

// بعد
IAsyncEnumerable<int> GetItems() { ... }

MIG015 — TimeoutController مُكتشَف

الخاصيةالقيمة
الخطورةمعلومة
الفئة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.