ما هو Roslyn ── قراءة شيفرة C# وإصلاحها وتوليدها من منظور المُصرِّف (compiler)

· آخر تحديث: · · .NET, CSharp, Roslyn, Analyzer, SourceGenerator, Compiler, StaticAnalysis, CodeGeneration, الاستفادة من الأصول القائمة

1. الأساسيّات الواجب الإلمام بها أوّلاً

هناك مواقف كثيرة، أكثر ممّا نتصوّر، تستدعي معالجة الشيفرة المصدريّة لـ C#. على سبيل المثال، هذه الأعمال:

أريد حظر طريقة استخدام واجهة API معيّنة
أريد اكتشاف أسلوب كتابة قديم آليّاً
أريد جمع قائمة بالتوابع والصفوف
أريد فحص التبعيّات (dependencies) عبر المشروع كلّه
أريد توليد شيفرة نمطيّة وقت التصريف
أريد تحذير المستخدِمين عند سوء استخدام مكتبتنا وقت البناء
أريد إجراء استبدال أو انتقال واسع النطاق بأمان

في مثل هذه الحالات، قد يخطر ببالك ببساطة فتح ملفّات *.cs ومعالجتها عبر بحث نصّي أو تعبير نمطيّ (regular expression).

لكنّ C# ليس نصّاً. هذان المثالان يتشابهان في الشكل لكنّهما مختلفان تماماً من حيث المعنى.

Console.WriteLine("Hello");
MyCompany.Logging.Console.WriteLine("Hello");

كما أنّ الاسم Console قد يشير إلى نوع (type) آخر تماماً.

using Console = MyCompany.Logging.Console;

Console.WriteLine("Hello");

من منظور النصّ، تبدو كلّها Console.WriteLine. لكن من منظور المُصرِّف، لا يمكن معرفة ما إذا كانت System.Console.WriteLine أم نوعاً آخر إلّا بعد تحليل الأسماء (name resolution).

هنا يظهر Roslyn. Roslyn هو منصّة تتيح استخدام المعلومات التي يملكها مُصرِّف C# وVisual Basic كواجهة API من داخل التطبيقات والأدوات.

باختصار، باستخدام Roslyn، يمكن التعامل مع شيفرة C# كالتالي:

قراءتها كبنية نحويّة، لا كسلسلة نصوص
قراءتها من حيث المعنى، لا من حيث الشكل
قراءتها كمشروع أو حلّ (solution)، لا كملفّ واحد
إصدار تحذيرات ومقترحات إصلاح وشيفرة مولَّدة بناءً على نتيجة القراءة

في هذا المقال، نرتّب الصورة الكاملة لـ Roslyn، وSyntax Tree، وSemanticModel، وWorkspace، وAnalyzer، وSource Generator، ومواضع استخدامه في العمل الفعليّ.

كما أنّ الشيفرة الواردة في هذا المقال منشورة على GitHub كمجموعة أمثلة كاملة قابلة للبناء والتشغيل (مكتبة تتعامل مع Syntax Tree / SemanticModel، وAnalyzer يحذِّر من DateTime.Now، وSource Generator، وعرض توضيحيّ لتحليل الحلّ (solution) بأكمله، واختبارات وحدة (unit tests) للتحقّق من الاكتشاف الخاطئ والاكتشاف المفقود).

roslyn-dotnet-compiler-platform - komurasoft-blog-samples (GitHub)

2. ما هو Roslyn

اسم Roslyn الرسميّ هو .NET Compiler Platform. وهو تطبيق (implementation) لمُصرِّف C# وVisual Basic، وفي الوقت نفسه مجموعة من واجهات API لبناء أدوات تحليل الشيفرة.

تقليديّاً، كان يُنظَر إلى المُصرِّف على أنّه «صندوق أسود» بهذا الشكل:

تُدخِل الشيفرة المصدريّة
يعالجها المُصرِّف
يخرج DLL / EXE

ولم يكن بمقدور المطوّر عادةً الوصول إلى المعلومات التي يُنشئها المُصرِّف داخليّاً.

لكنّ المُصرِّف الفعليّ لا يكتفي بتحويل النصّ إلى شيفرة آليّة أو IL، بل يُنشئ أثناء التصريف معلومات من هذا القبيل:

هذا النصّ إعلان صفّ (class)
هذا المعرِّف متغيّر محليّ
هذا الاستدعاء يشير إلى هذا التابع في هذا النوع
نوع القيمة المُعادة لهذا التعبير هو string
هذه الشيفرة تحتوي خطأً نحويّاً
هذا المرجع يشير إلى نوع في الأسمبلي (assembly) A
هذا الـ using غير مستخدَم فعليّاً

يتيح Roslyn للمطوّرين استخدام هذه المعلومات. لذلك، فإنّ Roslyn ليس مجرَّد مُصرِّف، بل منصّة لفهم الشيفرة.

3. ماذا يمكن فعله باستخدام Roslyn

باستخدام Roslyn، يمكن بشكل أساسيّ فعل ما يلي:

التحليل النحويّ لـ C# / VB
التحليل الدلاليّ للأنواع والتوابع
الحصول على معلومات التصريف
تحليل المشروع أو الحلّ (solution) بأكمله
إنشاء Analyzer خاصّ بك
إنشاء Code Fix
إنشاء Source Generator
إنشاء أداة إعادة هيكلة (refactoring)
توليد الشيفرة
تحويل الشيفرة

وبصياغة أقرب للعمل الفعليّ:

تحويل استخدام واجهة API محظورة إلى تحذير بناء (build warning)
حصر مواضع استخدام واجهة API قديمة
التحقّق من قاعدة تسمية توابع async
اكتشاف حالات إهمال معالجة IDisposable
توجيه الاستخدام الصحيح لإطار العمل الخاصّ بالشركة
توليد شيفرة DTO أو شيفرة تعيين (mapping) وقت التصريف
توليد شيفرة نمطيّة من ملفّات إعدادات أو سمات (attributes)
المساعدة في تحقيق الانتقال من .NET Framework إلى .NET

أهميّة Roslyn تكمن في إمكانيّة كتابة «المعالجة التي تتعامل مع شيفرة C# المصدريّة» على نفس أساس المُصرِّف نفسه.

قراءة C# عبر تعبيرات نمطيّة أو مُحلِّل (parser) خاصّ سرعان ما يصل إلى حدوده. فمن الصعب مثلاً التعامل الصحيح مع هذه العناصر:

using alias
التوابع الامتداديّة (extension methods)
partial class
partial method
global using
تعليقات nullable (nullable annotations)
الأنواع العامّة (generic types)
حلّ التحميل الزائد (overload resolution)
التصريف الشرطيّ (conditional compilation)
توجيهات المعالج المسبق (preprocessor directives)
إعادة الكتابة مع الحفاظ على التعليقات والمسافات

يوفِّر Roslyn واجهات API للتعامل مع هذه العناصر وفق مواصفات لغة C#.

4. يفصل Roslyn بين «البنية النحويّة» و«المعنى»

عند فهم Roslyn، أوّل ما يجدر فصله هو هذان الأمران:

البنية النحويّة (syntax): كيف كُتِبَت الشيفرة
المعنى (semantics): إلى ماذا تشير هذه الشيفرة

على سبيل المثال، انظر إلى هذه الشيفرة.

Console.WriteLine(message);

من حيث البنية النحويّة، شكلها كالتالي:

عبارة تعبيريّة
  تعبير استدعاء
    تعبير وصول إلى عضو
      معرِّف Console
      معرِّف WriteLine
    وسيطة message

لكنّ هذا وحده لا يكفي لمعرفة المعنى. لا يمكن الحكم من البنية النحويّة وحدها على نوع Console، ولا على أيّ تحميل زائد (overload) يمثِّله WriteLine، ولا على نوع message.

لمعرفة المعنى، يلزم توفّر هذه المعلومات:

حالة using
الأسمبلي (assembly) المرجعيّ
تعريفات الأنواع داخل المشروع نفسه
المراجع إلى مشاريع أخرى
استنتاج الأنواع (type inference)
حلّ التحميل الزائد (overload resolution)
إصدار اللغة
سياق nullable

في Roslyn، ينعكس هذا الفارق أيضاً على مستوى واجهة API:

Syntax Tree     : يمثِّل البنية النحويّة للشيفرة
SemanticModel   : يمثِّل معنى البنية النحويّة
Compilation     : يمثِّل كامل المعلومات اللازمة للتصريف
Workspace       : يتعامل مع الحلّ (solution) والمشروع والمستند

استيعاب هذا التمييز يجعل الرؤية العامّة لـ Roslyn أوضح كثيراً.

5. ما هو Syntax Tree

Syntax Tree هو شجرة تمثِّل البنية النحويّة للشيفرة المصدريّة. لنفترض وجود شيفرة كهذه:

class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}

من منظور Roslyn، تكون بنية هذه الشيفرة تقريباً كالتالي:

CompilationUnit
  ClassDeclaration: User
    PropertyDeclaration: Name
    MethodDeclaration: Rename
      Parameter: name
      Block
        ExpressionStatement
          AssignmentExpression

Syntax Tree ليس مجرَّد تقسيم للنصّ سطراً سطراً، بل بنية مرتَّبة كعناصر نحويّة في C#: الصفوف والتوابع والخصائص والتعبيرات والعبارات (statements) والوسائط (arguments) والعوامل (operators) وما إلى ذلك.

لنرَ مثالاً بسيطاً.

using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
class User
{
    public string Name { get; set; }

    public void Rename(string name)
    {
        Name = name;
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);
var root = tree.GetCompilationUnitRoot();

var methods = root
    .DescendantNodes()
    .OfType<MethodDeclarationSyntax>();

foreach (var method in methods)
{
    Console.WriteLine(method.Identifier.Text);
}

تبحث هذه الشيفرة عن إعلانات التوابع داخل الشيفرة المصدريّة وتُخرِج أسماءها. في هذا المثال، يمكن الحصول على Rename.

المهمّ هنا هو أنّنا لا نبحث عن void كنصّ، بل نبحث عن «إعلان تابع» كبنية نحويّة في C#.

6. Node وToken وTrivia

عند التعامل مع Syntax Tree، تتكرّر هذه المصطلحات الثلاثة:

SyntaxNode
SyntaxToken
SyntaxTrivia

SyntaxNode

SyntaxNode هو وحدة نحويّة متكاملة. مثل هذه العناصر:

إعلان صفّ (class)
إعلان تابع
إعلان خاصيّة
عبارة if
عبارة for
تعبير إسناد
تعبير استدعاء
تعبير lambda

كلّ عنصر نحويّ في C# يحتوي عناصر فرعيّة إضافيّة يُعدّ Node.

SyntaxToken

SyntaxToken هو أصغر وحدة تُكوِّن البنية النحويّة. مثل هذه العناصر:

الكلمة المفتاحيّة class
الكلمة المفتاحيّة public
المعرِّف User
المعرِّف Rename
{ أو }
; أو ,
النصّ الحرفيّ (string literal)
الرقم الحرفيّ (numeric literal)

الـ Token هو العنصر الموجود في أطراف شجرة البنية النحويّة.

SyntaxTrivia

SyntaxTrivia هي معلومات لا تتعلّق مباشرةً بالتحليل الدلاليّ المعتاد، لكنّها ضروريّة لإعادة إنتاج الشيفرة المصدريّة. مثل هذه العناصر:

المسافات البيضاء
الأسطر الجديدة
التعليقات
توجيهات المعالج المسبق

بفضل هذا الـ Trivia، يستطيع Roslyn التعامل مع الشيفرة المصدريّة بأمانة عالية، بما في ذلك التعليقات والمسافات.

يُعدّ الـ Trivia مهمّاً جدّاً عند تنسيق الشيفرة، وإعادة الهيكلة، والإعادة الكتابة الآليّة.

قد يبدو أنّه يمكن التخلّص من التعليقات إذا كان الهدف مجرَّد بناء AST. لكن في تحويلات الشيفرة الفعليّة، من المهمّ عدم إفساد التعليقات والأسطر الجديدة.

7. Syntax Tree غير قابل للتغيير (immutable)

شجرة Syntax Tree في Roslyn غير قابلة للتغيير. أي أنّه بدل تعديل شجرة البنية النحويّة التي حصلتَ عليها مباشرةً، يتمّ إنشاء شجرة نحويّة جديدة تحمل التغيير.

على سبيل المثال، عند الرغبة بتغيير اسم تابع، لا يتمّ تعديل MethodDeclarationSyntax القائم في مكانه.

var newMethod = oldMethod.WithIdentifier(
    SyntaxFactory.Identifier("NewName"));

بهذا الشكل، يُنشَأ عقدة (node) جديدة.

لعدم القابليّة للتغيير عدّة فوائد:

سهولة التعامل عبر عدّة خيوط (threads)
التعامل الآمن مع لقطة (snapshot) قيد التحرير في بيئة التطوير
سهولة إنشاء الفروقات (diffs)
سهولة مقارنة ما قبل التغيير وما بعده

قد يبدو الأمر مزعجاً بعض الشيء في البداية. لكن في عالم يتشارك فيه معالجات متعدّدة (IDE، والبناء، وAnalyzer، وSource Generator) نفس الشيفرة في آنٍ واحد، تصبح عدم القابليّة للتغيير ميزة كبيرة.

8. ما هو SemanticModel

لا يتيح Syntax Tree وحده معرفة سوى شكل الشيفرة. لمعرفة المعنى، يُستخدَم SemanticModel.

على سبيل المثال، انظر إلى هذه الشيفرة.

Console.WriteLine("Hello");

من Syntax Tree، يمكن معرفة وجود معرِّف باسم Console ومعرِّف باسم WriteLine. لكن لا يمكن معرفة ما إذا كانا يشيران إلى System.Console.WriteLine(string?) أم إلى تابع من نوع آخر.

باستخدام SemanticModel، يمكن الحصول على «الرمز (symbol) الذي حُلَّت إليه هذه العقدة النحويّة».

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;

var source = """
using System;

class Program
{
    static void Main()
    {
        Console.WriteLine("Hello");
    }
}
""";

var tree = CSharpSyntaxTree.ParseText(source);

var compilation = CSharpCompilation.Create(
    assemblyName: "Sample",
    syntaxTrees: new[] { tree },
    references: new[]
    {
        MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
        MetadataReference.CreateFromFile(typeof(Console).Assembly.Location)
    });

var semanticModel = compilation.GetSemanticModel(tree);
var root = tree.GetCompilationUnitRoot();

var invocation = root
    .DescendantNodes()
    .OfType<InvocationExpressionSyntax>()
    .First();

var symbolInfo = semanticModel.GetSymbolInfo(invocation);
var method = (IMethodSymbol?)symbolInfo.Symbol;

Console.WriteLine(method?.ContainingType.ToDisplayString());
Console.WriteLine(method?.Name);

بهذا الشكل، يمكن معرفة أيّ تابع حُلَّ إليه Console.WriteLine فعليّاً.

قوّة Roslyn تكمن في إمكانيّة استخدام نتيجة تحليل الأسماء لدى المُصرِّف، وليس البنية النحويّة فقط.

9. ما هو Symbol

في Roslyn، تُعامَل الأنواع والتوابع والخصائص والحقول والوسائط والمتغيّرات المحليّة وغيرها كـ Symbol.

من الواجهات الممثِّلة لذلك:

INamedTypeSymbol  : الصفوف والبُنى والواجهات وغيرها
IMethodSymbol     : التوابع (methods)
IPropertySymbol   : الخصائص (properties)
IFieldSymbol      : الحقول (fields)
IParameterSymbol  : الوسائط (parameters)
ILocalSymbol      : المتغيّرات المحليّة
INamespaceSymbol  : مساحات الأسماء (namespaces)

الـ Symbol يمثِّل ليس شكل الشيفرة المصدريّة الظاهر، بل المعنى الذي حلَّه المُصرِّف.

على سبيل المثال، تختلف هاتان الشيفرتان في الشكل.

System.Console.WriteLine("Hello");
using System;

Console.WriteLine("Hello");

لكن إن كانتا تشيران إلى نفس System.Console.WriteLine، فإنّ التحليل الدلاليّ في Roslyn يتعامل معهما كرمز تابع (method symbol) واحد.

بفضل هذه الخاصيّة، يمكن إجراء أحكام من هذا النوع:

هل هذا الاستدعاء يستخدم فعلاً واجهة API المحظورة داخليّاً
هل هذا النوع ينفِّذ واجهة معيّنة
هل هذا التابع async
هل القيمة المُعادة nullable
هل هذا السمة (attribute) مُضافة فعلاً
هل يرث هذا الصفّ من صفّ أساسيّ معيّن

يمكن إجراء تحليل استناداً إلى حكم المُصرِّف، لا مجرَّد بحث نصّي.

10. ما هو Compilation

Compilation هو تجميع للمعلومات اللازمة لتصريف برنامج C# أو Visual Basic.

يحمل تحديداً هذه المعلومات:

مجموعة SyntaxTree
الأسمبلي (assembly) المرجعيّ
خيارات التصريف
إصدار اللغة
الرموز المُعرَّفة مسبقاً
معلومات الأنواع والأعضاء
معلومات التشخيص (diagnostics)

يكفي SyntaxTree وحده لقراءة ملفّ واحد كبنية نحويّة. لكن لإجراء حلّ الأنواع أو حلّ المراجع، يلزم Compilation.

على سبيل المثال، عند الرغبة بمعرفة ما يلي:

أريد معرفة أيّ نوع يخصّ هذا الاستدعاء
أريد معرفة ما إذا كان هذا الصفّ ينفِّذ IDisposable
أريد معرفة أيّ نوع سمة (attribute) هذا فعليّاً
أريد معرفة نوع القيمة المُعادة لهذا التعبير
أريد الحصول على أخطاء أو تحذيرات التصريف

لا يمكن الحكم على هذه الأمور بالبنية النحويّة وحدها. يلزم النظر أيضاً إلى مراجع المشروع وخيارات التصريف.

11. ما هو Workspace

عند الرغبة بالتعامل مع الحلّ (solution) أو المشروع بأكمله، لا ملفّاً واحداً فقط، يُستخدَم Workspace.

يتعامل Workspace مع هذه الوحدات:

Solution
Project
Document

على سبيل المثال، حالة قراءة جميع المشاريع من الحلّ بأكمله وتحليل جميع المستندات.

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync("Sample.sln");

foreach (var project in solution.Projects)
{
    Console.WriteLine(project.Name);

    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        Console.WriteLine($"  {document.Name}: {root?.DescendantNodes().Count()} nodes");
    }
}

يمكن استخدام أداة كهذه في تحقيق قاعدة الشيفرة القائمة ودعم الانتقال.

على سبيل المثال، في استخدامات كهذه:

تحويل قائمة استدعاءات واجهة API معيّنة إلى CSV
حصر مواضع استخدام مساحة أسماء قديمة
إنشاء قائمة بواجهات API العامّة (public)
فحص التبعيّات بين المشاريع
فحص مخالفات قواعد الشيفرة في حلّ ضخم
إجراء تحويل شيفرة آليّ

الـ Analyzer آليّة تعمل بتكامل مع بيئة التطوير (IDE) والبناء. أمّا أداة سطر الأوامر التي تستخدم Workspace فتناسب التحقيق والانتقال الجماعيّ.

هذان متشابهان لكن يُستحسَن الفصل بين مواضع استخدامهما.

12. الأشكال الشائعة لاستخدام Roslyn

تنقسم طرق استخدام Roslyn إجمالاً إلى أربعة:

1. الاستخدام كمكتبة
2. إنشاء Analyzer
3. إنشاء Code Fix
4. إنشاء Source Generator

لكلّ منها غرض مختلف.

الاستخدام كمكتبة

استدعاء واجهات API الخاصّة بـ Roslyn من تطبيق سطر أوامر أو أداة داخليّة من صنعك.

الاستخدامات المناسبة لهذا الشكل:

تحقيق قاعدة الشيفرة
التحويل الجماعيّ
جمع المقاييس (metrics)
دعم الانتقال
إعداد التقارير

في هذا الشكل، يمكن تشغيل الأداة في أيّ وقت تشاء. ولأنّها لا تحتاج للعمل أثناء الكتابة في بيئة التطوير، فمن الأسهل قبول معالجة ثقيلة نسبيّاً.

إنشاء Analyzer

الـ Analyzer آليّة تحلِّل الشيفرة وتُصدِر تحذيرات أو أخطاءً.

على سبيل المثال، يمكن إنشاء قواعد كهذه:

استخدام DateTimeOffset.UtcNow بدل DateTime.Now
أسماء توابع async تنتهي بـ Async
عدم استدعاء واجهة تهيئة المكتبة بترتيب خاطئ
عدم استخدام مساحة أسماء معيّنة في الشيفرة الجديدة
حظر استخدام Task.Result / Wait

يمكن تشغيل الـ Analyzer في Visual Studio أو أثناء البناء. يتيح ذلك اكتشاف قواعد الفريق وطريقة استخدام المكتبة آليّاً، دون الاعتماد على ذاكرة مراجع الكود.

إنشاء Code Fix

الـ Code Fix آليّة تقدِّم مقترح إصلاح للمشكلة التي عثر عليها الـ Analyzer.

يسهُل تخيّله بالتفكير في الإصلاح الذي يمكن تطبيقه من أيقونة المصباح في Visual Studio.

لنفترض أنّ الـ Analyzer اكتشف هذه الشيفرة.

DateTime.Now

يمكن لِـ Code Fix تقديم إصلاح كهذا.

DateTimeOffset.UtcNow

قوّة Code Fix لا تكمن فقط في «إصدار تحذير»، بل في أتمتة «طريقة الإصلاح الآمنة».

إنشاء Source Generator

الـ Source Generator آليّة تولِّد شيفرة وقت التصريف وتضيف تلك الشيفرة إلى التصريف نفسه.

على سبيل المثال، استخدامات كهذه:

توليد شيفرة نمطيّة من صفوف تحمل سمة (attribute) معيّنة
توليد وصول (accessor) آمن النوع من ملفّ إعدادات
توليد شيفرة تعيين (mapping) لكائنات DTO
توليد شيفرة لأغراض التسلسل (serializer)
توليد شيفرة تحويل بين enum ونصّ
توليد شيفرة توجيه (routing) أو تسجيل DI

في بعض الحالات، يمكن استبدال معالجة كانت تجمع معلومات عبر Reflection وقت التشغيل بشيفرة مولَّدة وقت التصريف. قد يؤدّي هذا إلى تقليل تكلفة بدء التشغيل وتحسين التوافق مع AOT.

13. يمكن استخدام Analyzer بوصفه «مراجعة شيفرة آليّة»

في العمل الفعليّ، يسهُل فهم الـ Analyzer باعتباره «مراجعة شيفرة آليّة».

في مراجعة الشيفرة، قد تتكرّر نفس الملاحظة في كلّ مرّة.

لا تستخدم هذه الواجهة (API)
اسم هذا التابع لا يتوافق مع القاعدة
هذا الـ catch يبتلع الاستثناء
هذا التحقّق من null غير ضروريّ
هذا الاستدعاء يسبِّب مشكلة في الأداء

إذا كان الإنسان يكرِّر نفس الملاحظة في كلّ مرّة، فمن الممكن تحويل جزء منها إلى Analyzer.

القواعد المناسبة بشكل خاصّ لِـ Analyzer هي هذه:

يمكن الحكم على صحّتها بوضوح
الاستثناءات قليلة
سياسة الإصلاح محدَّدة
يريد الفريق كلّه الالتزام بها
تتكرّر كثيراً في المراجعة
يجوز إيقاف البناء بسببها

على العكس، هناك قواعد لا تناسب Analyzer.

الحكم صعب ويعتمد على السياق
تحتاج إلى قرار تصميميّ
الاستثناءات كثيرة جدّاً
تختلف الآراء باختلاف الأشخاص
كثرة التحذيرات تجعل الجميع يتجاهلونها

الـ Analyzer أداة قويّة. ولأنّها أداة قويّة، فإنّ الإفراط في إدراجها يُسيء إلى تجربة التطوير. من الأفضل البدء بعدد قليل من القواعد المهمّة.

14. ابدأ بـ Analyzer المُضمَّن في .NET SDK

قبل كتابة Analyzer خاصّ بك، من الواقعيّ التحقّق أوّلاً من الـ Analyzer المُضمَّن في .NET SDK.

في مشاريع .NET 5 فما بعد، يكون تحليل شيفرة .NET مُفعَّلاً افتراضيّاً.

من معرِّفات التشخيص (diagnostic IDs) الشائعة هاتان الفئتان:

CAxxxx : جودة الشيفرة، الموثوقيّة، الأداء، الأمان وما شابه
IDExxxx: نمط الشيفرة، دعم بيئة التطوير وما شابه

يمكن ضبط مستوى خطورة الـ Analyzer عبر .editorconfig.

# مثال: تحويل using غير المستخدَم إلى تحذير
dotnet_diagnostic.IDE0005.severity = warning

# مثال: تحويل CA2000 إلى خطأ
dotnet_diagnostic.CA2000.severity = error

يمكن أيضاً تفعيله أو تشديده من ملفّ المشروع.

<PropertyGroup>
  <EnableNETAnalyzers>true</EnableNETAnalyzers>
  <AnalysisLevel>latest</AnalysisLevel>
  <TreatWarningsAsErrors>false</TreatWarningsAsErrors>
</PropertyGroup>

عند إدخاله في مشروع قائم، قد تظهر في البداية كمّيّة كبيرة من التحذيرات. في هذه الحالة، من الأفضل التقدّم تدريجيّاً بدل تحويل كلّ شيء إلى خطأ دفعةً واحدة.

معرفة عدد التحذيرات الإجماليّ أوّلاً
سياسة عدم زيادتها في الشيفرة الجديدة
تحويل القواعد المهمّة فقط إلى warning
تحويل القواعد التي يجب الالتزام بها فعلاً فقط إلى error
تقليل المخالفات القائمة بشكل مخطَّط له

يمكن اعتبار Analyzer الخاصّ بك مكمِّلاً لِـ «قواعد خاصّة بالشركة» فوق هذه القاعدة.

15. الصورة الأدنى لِـ Analyzer

يبحث الـ Analyzer عن بنية نحويّة أو رمز (symbol) معيّن ويُبلِغ عن Diagnostic.

على سبيل المثال، لنفكِّر في Analyzer يحذِّر من استخدام DateTime.Now.

في شيفرة المنتج الفعليّة، يلزم التعامل بعناية مع حلّ الأنواع والحالات الاستثنائيّة، لكنّ الصورة الأدنى تكون كالتالي:

using System.Collections.Immutable;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class NoDateTimeNowAnalyzer : DiagnosticAnalyzer
{
    private static readonly DiagnosticDescriptor Rule = new(
        id: "CMP001",
        title: "لا تستخدم DateTime.Now مباشرةً",
        messageFormat: "بدلاً من DateTime.Now، فكِّر في استخدام DateTimeOffset.UtcNow أو ما شابه حسب الغرض",
        category: "Usage",
        defaultSeverity: DiagnosticSeverity.Warning,
        isEnabledByDefault: true);

    public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        => ImmutableArray.Create(Rule);

    public override void Initialize(AnalysisContext context)
    {
        context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
        context.EnableConcurrentExecution();

        context.RegisterSyntaxNodeAction(
            AnalyzeMemberAccess,
            SyntaxKind.SimpleMemberAccessExpression);
    }

    private static void AnalyzeMemberAccess(SyntaxNodeAnalysisContext context)
    {
        var memberAccess = (MemberAccessExpressionSyntax)context.Node;

        if (memberAccess.Name.Identifier.Text != "Now")
        {
            return;
        }

        var symbol = context.SemanticModel.GetSymbolInfo(memberAccess).Symbol;
        if (symbol is not IPropertySymbol propertySymbol)
        {
            return;
        }

        if (propertySymbol.Name == "Now" &&
            propertySymbol.ContainingType.ToDisplayString() == "System.DateTime")
        {
            var diagnostic = Diagnostic.Create(Rule, memberAccess.GetLocation());
            context.ReportDiagnostic(diagnostic);
        }
    }
}

المهمّ في هذا المثال أنّه لا يبحث عن DateTime.Now كنصّ فقط.

بل يستخدم SemanticModel للتحقّق فعليّاً من كونها تشير إلى System.DateTime.Now.

لذلك، يصعب الوقوع في اكتشاف خاطئ لعناصر مختلفة تماماً مثل هذه:

MyCompany.DateTime.Now

في الـ Analyzer، يُستخدَم هذا التدفّق كثيراً: تضييق المرشَّحين بسرعة عبر Syntax، ثمّ التحقّق منهم بدقّة عبر التحليل الدلاليّ.

البحث السريع عن المرشَّحين عبر Syntax
الحكم الدقيق عبر SemanticModel
إبلاغ الموقع والرسالة عبر Diagnostic

16. Code Fix آليّة توفِّر «طريقة الإصلاح» أيضاً

يعثر الـ Analyzer على المشكلة، ويقدِّم الـ Code Fix طريقة إصلاح تلك المشكلة.

على سبيل المثال، عند اكتشاف DateTime.Now، يمكن تقديم مقترحات إصلاح كهذه:

الاستبدال بـ DateTimeOffset.UtcNow
الاستبدال بتجريد (abstraction) مثل IClock.Now

لكن يجب تصميم Code Fix بعناية. فليس صحيحاً دائماً أنّ استبدال DateTime.Now بـ DateTimeOffset.UtcNow هو الحلّ. يختلف النوع والمنطقة الزمنيّة المناسبة باختلاف الغرض: عرض الوقت المحلّي أم التعامل مع وقت مخصَّص للحفظ والمقارنة.

لذلك، يناسب Code Fix الحالات التي تحقِّق هذه الشروط:

معنى ما بعد الإصلاح واضح
الآثار الجانبيّة صغيرة
يمكن الإصلاح بأمان عبر تحويل آليّ
سهل التحقّق للإنسان

على سبيل المثال، تتناسب هذه الأنواع من الإصلاحات جيّداً مع Code Fix:

استبدال اسم واجهة API قديم باسم جديد
إضافة using ناقص
تغيير الاسم وفق قاعدة التسمية
إضافة سمة (attribute)
حذف وسيطة (argument) غير ضروريّة

من ناحية أخرى، قد يكون الاكتفاء بالتحذير أفضل من الإصلاح الآليّ للتعديلات التي تحتاج إلى قرار تصميميّ.

17. Source Generator هو «توليد شيفرة وقت التصريف»

يعمل Source Generator وقت التصريف، ويضيف شيفرة C# المولَّدة إلى نفس التصريف.

يمكن تخيّل التدفّق كالتالي:

قراءة الشيفرة المصدريّة الخاصّة بالمستخدِم
فحص السمات (attributes) أو تعريفات الأنواع
توليد شيفرة C# اللازمة
إضافة الشيفرة المولَّدة إلى هدف التصريف

مثال بسيط على Source Generator.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

[Generator]
public sealed class BuildInfoGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(static ctx =>
        {
            var source = """
namespace Generated;

public static class BuildInfo
{
    public static string Tool => "Roslyn Source Generator";
}
""";

            ctx.AddSource(
                "BuildInfo.g.cs",
                SourceText.From(source, Encoding.UTF8));
        });
    }
}

في المشروع الذي يشير إلى هذا الـ Generator، يمكن استخدام هذا النوع حتّى لو لم تُكتَب ملفّ شيفرة مصدريّة له.

Console.WriteLine(Generated.BuildInfo.Tool);

الـ Source Generator ليس شيئاً يُعيد كتابة شيفرة المستخدِم القائمة. كلّ ما يمكنه فعله هو توليد شيفرة مصدريّة إضافيّة وإشراكها في التصريف.

لذلك، من المفيد التفكير كالتالي:

ليس شيئاً يحوِّل الشيفرة القائمة
بل شيئاً ينظر إلى الشيفرة القائمة وينشئ شيفرة إضافيّة

عند الرغبة بإعادة كتابة الشيفرة القائمة جماعيّاً، فكِّر في أداة انتقال تستخدم Roslyn أو Code Fix، بدل Source Generator.

18. طريقة الإشارة إلى Source Generator

عند الإشارة إلى مشروع Generator من مشروع آخر أثناء التطوير، يختلف التعامل عن الإشارة المكتبيّة العاديّة.

ذلك لأنّ المولِّد (generator) ليس مكتبة تُشار إليها وقت التشغيل، بل يُحمَّل كـ Analyzer وقت التصريف.

في إشارة المشروع، يُحدَّد كالتالي:

<ItemGroup>
  <ProjectReference Include="..\BuildInfoGenerator\BuildInfoGenerator.csproj"
                    OutputItemType="Analyzer"
                    ReferenceOutputAssembly="false" />
</ItemGroup>

بضبط ReferenceOutputAssembly="false"، يُمنَع التعامل مع DLL الخاصّ بالـ Generator كأسمبلي (assembly) مرجعيّ عاديّ.

عند التوزيع كحزمة NuGet أيضاً، يُوضَع بحيث يُحمَّل كـ Analyzer / Source Generator.

الـ Source Generator مفيد، لكن دورة حياته تختلف عن المكتبة العاديّة.

المكتبة العاديّة: تُستخدَم من التطبيق وقت التشغيل
Source Generator: يُستخدَم من المُصرِّف وقت التصريف

من المهمّ الانتباه لهذا الفارق.

19. ما يناسب Source Generator

لا يعني Source Generator أنّه يمكن توليد أيّ شيء بشكل مناسب.

ما يناسبه هو هذا النوع من الشيفرة:

تكتب مملّة وسهلة الخطأ عند كتابتها يدويّاً
تتحدَّد آليّاً من معلومات المُدخَل
النتيجة المولَّدة سهلة القراءة
يمكن تقليل Reflection وقت التشغيل
يمكن تحسين التوافق مع AOT والتقليم (trimming)
يمكن رفع مستوى أمان الأنواع

أمثلة على ذلك:

البيانات الوصفيّة (metadata) لتسلسل JSON
شيفرة تسجيل DI
مُوصِّلات (accessors) قيم الإعدادات
عميل واجهة API
شيفرة تحويل enum
توليد أنواع من تعريفات SQL أو CSV
شيفرة مساعدة لِـ INotifyPropertyChanged

لكن إن كانت الشيفرة المولَّدة معقّدة أكثر من اللازم، يصعب تتبّعها عند حدوث مشكلة.

عند استخدام Source Generator، من المفيد الانتباه إلى ما يلي:

جعل الشيفرة المولَّدة قابلة للتحقّق
تثبيت اسم الشيفرة المولَّدة
جعل نتيجة التوليد حتميّة (deterministic)
جعل رسالة Diagnostic عند الخطأ واضحة
تجنّب ظهور فروقات ضخمة لمجرَّد تغيير بسيط في المُدخَل

يجب ألّا تبدو الشيفرة المولَّدة سحراً. المهمّ هو إصدار شيفرة يستطيع الصائن (maintainer) المستقبليّ قراءتها.

20. ما لا يناسب Source Generator

هناك أيضاً معالجات لا تناسب Source Generator.

معالجة تعتمد على حالة وقت التشغيل
معالجة تحتاج وصولاً إلى الشبكة
معالجة تعتمد على القيمة الحاليّة لخدمة خارجيّة
معالجة تتغيّر نتيجتها في كلّ مرّة
إعادة كتابة الشيفرة المصدريّة القائمة
تحليل حلّ (solution) ضخم بأكمله

بما أنّ الـ Generator يعمل وقت التصريف، فإنّ الـ Generator البطيء يُسيء إلى زمن البناء وتجربة بيئة التطوير.

كما أنّ الـ Generator المعتمِد على البيئة يسبِّب مشكلات من هذا النوع:

يعمل على جهاز المطوّر لكن يفشل في CI
يعمل في CI لكن يفشل في نظام تشغيل آخر
تتغيّر النتيجة حسب حالة الذاكرة المؤقّتة (cache)
يفشل البناء بسبب عطل في الشبكة

من الأفضل جعل Source Generator أقرب ما يمكن إلى معالجة نقيّة (pure).

المُدخَل: الشيفرة المصدريّة، AdditionalFiles، AnalyzerConfigOptions
المُخرَج: شيفرة C# مولَّدة، Diagnostic

كلّما كانت هذه العلاقة واضحة، كان الـ Generator أكثر استقراراً.

21. أداة تحقيق الشيفرة باستخدام Roslyn

لا يقتصر استخدام Roslyn على Analyzer وSource Generator. استخدام Roslyn من أداة سطر أوامر خاصّة بك فعّال أيضاً في العمل الفعليّ.

على سبيل المثال، متطلّبات كهذه:

أريد حصر مواضع استخدام واجهة API قديمة
أريد عدّ عدد صفوف public لكلّ مشروع
أريد تحويل الصفوف التي تحمل سمة (attribute) معيّنة إلى CSV
أريد فحص مساحات الأسماء التي يعتمد عليها حلّ ضخم
أريد حصر واجهات API التي تعتمد على Windows قبل الانتقال من .NET Framework

في مثل هذه الحالات، قد يكون بناؤها كأداة تحقيق تُنفَّذ مرّة واحدة أو بشكل دوريّ أسهل تعاملاً من بنائها كـ Analyzer.

كمثال، صورة بسيطة لتعداد الصفوف العامّة (public class) داخل حلّ (solution).

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    foreach (var document in project.Documents)
    {
        var root = await document.GetSyntaxRootAsync();
        if (root is null)
        {
            continue;
        }

        var classes = root.DescendantNodes()
            .OfType<ClassDeclarationSyntax>()
            .Where(c => c.Modifiers.Any(m => m.Text == "public"));

        foreach (var cls in classes)
        {
            Console.WriteLine($"{project.Name},{document.FilePath},{cls.Identifier.Text}");
        }
    }
}

يكتفي هذا المثال بالنظر إلى البنية النحويّة فقط. إن أردتَ معرفة «الصفوف العامّة الموروثة من صفّ أساسيّ معيّن»، فيلزم استخدام SemanticModel للنظر إلى علاقة وراثة الأنواع.

إن كفى الاسم أو الشكل فقط، فاستخدم Syntax
إن لزم النوع أو المرجع، فاستخدم SemanticModel
إن لزم المشروع بأكمله، فاستخدم Workspace

هذا هو التمييز الأساسيّ.

22. الفرق مع التعبيرات النمطيّة

التعبيرات النمطيّة مفيدة، لكنّها لا تناسب التعامل مع معنى شيفرة C#.

على سبيل المثال، انظر إلى هذه الشيفرة.

// Console.WriteLine("debug");

عند البحث عن Console.WriteLine بتعبير نمطيّ، قد يُلتقَط النصّ الموجود داخل التعليق أيضاً.

توجد أيضاً نصوص حرفيّة (string literals) كهذه.

var text = "Console.WriteLine";

أو قد يُقسَّم على عدّة أسطر.

Console
    .WriteLine("Hello");

بل يمكن أيضاً استخدام اسم مستعار (alias).

using C = System.Console;

C.WriteLine("Hello");

يصعب على التعبيرات النمطيّة التعامل الصحيح مع هذه الحالات.

باستخدام Roslyn، يمكن التمييز بين التعليقات، والنصوص الحرفيّة، واستدعاءات التوابع على المستوى النحويّ، والتابع المُحلَّل فعليّاً.

بالطبع، قد يكفي grep أو ripgrep للتحقيق البسيط. لكن عند اتّخاذ قرارات تصميم أو إجراء إصلاحات آليّة بناءً على النتيجة، فإنّ استخدام Roslyn أكثر أماناً.

للبحث التقريبيّ فقط، يكفي البحث النصّي
عند الرغبة بالحكم الصحيح كشيفرة C#، استخدم Roslyn

23. استخدام Roslyn في تحقيق الأصول القائمة

عند الانتقال من .NET Framework إلى .NET الحاليّ، أوّل ما يلزم هو «معرفة الوضع الحاليّ». هنا يفيد Roslyn.

على سبيل المثال، تحقيقات كهذه:

قائمة الاعتماد على System.Web
قائمة الشيفرة المعتمِدة على App.config / Web.config
مواضع استخدام واجهات API خاصّة بـ Windows Forms / WPF
مواضع استخدام Remoting / BinaryFormatter
وجود مراجع COM من عدمه
قائمة P/Invoke
معالجة I/O غير غير متزامنة (non-async)
مواضع استخدام واجهات تشفير قديمة

يمكن للبحث النصّي البسيط أيضاً إظهار مرشَّحين. لكن باستخدام Roslyn، يمكن الحصول على قوائم استناداً إلى نتيجة الحلّ الفعليّ للأنواع والتوابع.

على سبيل المثال، إن اكتفينا بالبحث عن نصّ BinaryFormatter، فسنلتقط التعليقات والوثائق أيضاً.

باستخدام Roslyn للتحقّق من استخدام النوع System.Runtime.Serialization.Formatters.Binary.BinaryFormatter، يمكن إخراج مرشَّحين أكثر دقّة.

في أعمال الانتقال، لا حاجة لإنشاء Analyzer متكامل من البداية. مجرَّد إخراج CSV كهذا كأداة تحقيق يحمل قيمة بحدّ ذاته.

Project,File,Line,Symbol,Kind
Legacy.Web,Controllers/HomeController.cs,42,System.Web.HttpContext.Current,Property
Legacy.Core,Serialization/OldStore.cs,18,System.Runtime.Serialization.Formatters.Binary.BinaryFormatter,Type

وجود قائمة كهذه يسهِّل وضع خطّة الانتقال.

24. Roslyn بالنسبة لمطوّري المكتبات

Roslyn مفيد ليس فقط لمطوّري التطبيقات، بل أيضاً لمطوّري المكتبات. للمكتبات طريقة استخدام صحيحة.

على سبيل المثال، قواعد كهذه:

يجب استدعاء تابع التهيئة أوّلاً
يجب إضافة سمة (attribute) معيّنة
يجب استدعاء Dispose
تحديد خيار معيّن خطِر
لا نريد استخدام واجهة API مهجورة في الشيفرة الجديدة

إن نُقِلَت هذه بالتوثيق فقط، فقد يفوت المستخدِمون قراءتها.

بإرفاق Analyzer مع حزمة NuGet الخاصّة بالمكتبة، يمكن إصدار تحذير في شيفرة المستخدِم نفسها.

على سبيل المثال، لنفترض وجود مكتبة خاصّة بالشركة اسمها Company.Messaging، يمكن اكتشاف سوء استخدام كهذا:

var client = new MessageClient();
client.Send(message); // استدعاء Send قبل استدعاء Configure

يمكن للـ Analyzer إصدار تحذير كهذا:

CMP1001: يجب استدعاء Configure قبل استدعاء MessageClient.Send

بل يمكن أيضاً عبر Code Fix تقديم مقترح إصلاح أو شيفرة نموذجيّة. هذا يحسِّن تجربة استخدام المكتبة.

نقل ما هو مكتوب في التوثيق إلى محرِّر المستخدِم نفسه

هذه الفكرة تمثِّل قيمة كبيرة لـ Roslyn.

25. منهجيّة توزيع Analyzer عبر NuGet

يمكن توزيع Analyzer كحزمة NuGet. لكن يجب التفكير فيه منفصلاً عن مكتبة التشغيل العاديّة. فالـ Analyzer ليس ضروريّاً وقت تشغيل التطبيق، بل يُستخدَم وقت البناء أو في بيئة التطوير.

لذلك، في تصميم الحزمة، يُنظَر في نقاط كهذه:

هل تُضمَّن مكتبة التشغيل والـ Analyzer في نفس الحزمة
هل يُجعَل الـ Analyzer حزمة منفصلة
هل يُصدَر تحذير افتراضيّاً
كيف يكون مستوى الخطورة
هل يمكن التحكّم به عبر .editorconfig
عدم إصدار تحذيرات جماعيّة مفاجئة للمستخدِمين القائمين

إن كان الاستخدام داخليّاً فقط، فقد تُقبَل قواعد صارمة نسبيّاً.

أمّا عند التوزيع كمكتبة عامّة، فيلزم مراعاة عدم كسر بناء المستخدِمين فجأةً.

غالباً ما يكون البدء بمستوى Info أو Warning، مع إتاحة رفعه إلى Error من جهة المستخدِم عند الحاجة، أسهل تعاملاً.

26. Roslyn ووظائف بيئة التطوير (IDE)

في Visual Studio وبيئات تطوير .NET الأخرى، ترتبط فكرة Roslyn ارتباطاً عميقاً بوظائف بيئة التطوير أيضاً.

على سبيل المثال، وظائف كهذه:

IntelliSense
Go to Definition
Find All References
Rename
Extract Method
Quick Actions
تحذيرات نمط الشيفرة
اكتشاف using غير المستخدَم

لا يمكن تحقيق هذه الوظائف عبر بحث نصّي بسيط. على سبيل المثال، في Rename، يجب عدم تغيير رمز (symbol) آخر يحمل نفس الاسم عن طريق الخطأ.

class User
{
    public string Name { get; set; }
}

class Product
{
    public string Name { get; set; }
}

عند الرغبة بتغيير User.Name، لا يجوز تغيير Product.Name أيضاً. يلزم هنا التمييز كرمز (symbol)، لا كبنية نحويّة فقط.

تُعدّ واجهات API الخاصّة بـ Roslyn أساساً يمكن تطبيق مثل هذه الوظائف الشبيهة ببيئة التطوير عليه في أدواتك الخاصّة.

27. ملاحظات حول الأداء

Roslyn قويّ، لكنّ كتابة معالجة ثقيلة تجعله بطيئاً بطبيعة الحال. خصوصاً الـ Analyzer وSource Generator، فقد يعملان أثناء كتابة المطوّر أو أثناء البناء.

لذلك، انتبه إلى هذه النقاط:

تجنّب الحصول غير الضروريّ على SemanticModel
تضييق المرشَّحين عبر Syntax أوّلاً ثمّ التحليل الدلاليّ
تجنّب عمليّات I/O الملفّات
عدم الوصول إلى الشبكة
تجنّب Reflection الثقيل
احترام طلبات الإلغاء (cancellation)
مراعاة التنفيذ المتوازي
عدم إدخال تحليل الحلّ (solution) بأكمله داخل Analyzer

في الـ Analyzer، ضيِّق الأهداف المُسجَّلة في Initialize بقدر الإمكان.

مثال سيّئ:

النظر إلى كلّ SyntaxNode ثمّ الحكم داخليّاً عبر كمّيّة كبيرة من عبارات if

اتّجاه جيّد:

تسجيل SyntaxKind اللازم فقط
التضييق الخفيف أوّلاً بالاسم أو الشكل
التأكيد النهائيّ عبر SemanticModel فقط عند الحاجة

قد يبقى الـ Analyzer مقيماً داخل بيئة تطوير المستخدِم. لذلك فإنّ الخفّة ليست أقلّ أهميّة من الدقّة كمقياس جودة.

28. تصميم Diagnostic

الـ Diagnostic الذي يُصدِره Analyzer ليس مجرَّد إصدار تحذير. يجب أن يستطيع المطوّر عند رؤيته فهم هذه الأمور:

ما المشكلة
لماذا هي مشكلة
أين يجب الإصلاح
كيف يجب الإصلاح
هل توجد استثناءات

مثال على رسالة سيّئة:

CMP001: هذا محظور

لا يمكن معرفة ما هو الخطأ من هذه الرسالة.

مثال على اتّجاه جيّد:

CMP001: يعتمد DateTime.Now على التوقيت المحلّي لبيئة التشغيل. استخدم DateTimeOffset.UtcNow أو مزوّد وقت (time provider) للأوقات المخصَّصة للحفظ أو المقارنة.

من المفيد أيضاً تصميم معرِّف Diagnostic (Diagnostic ID).

CMP0001-CMP0999: القواعد المشترَكة
CMP1000-CMP1999: قواعد المكتبة A
CMP2000-CMP2999: قواعد دعم الانتقال

إن أمكن إعداد صفحة توثيق، فمن المفيد أيضاً ضبط HelpLinkUri في DiagnosticDescriptor.

التحذير هو تواصل مع المطوّر. إن كانت الرسالة مهملة، فسيفقد المطوّرون الثقة بالقاعدة نفسها أيضاً.

29. تصميم مستوى الخطورة (severity)

يجب تحديد مستوى خطورة Analyzer بعناية. المراحل التمثيليّة هي:

Hidden / Silent
Info
Suggestion
Warning
Error

في العمل الفعليّ، غالباً ما يكون من الأفضل عدم جعلها Error مباشرةً. خصوصاً عند وجود شيفرة قائمة كثيرة، فإنّ تحويلها إلى Error من البداية يوقف عمليّة التبنّي.

من الناحية العمليّة، يسهُل التبنّي التدريجيّ كهذا:

1. البدء بمستوى Warning أوّلاً
2. إظهار عدد التحذيرات بوضوح في CI
3. عدم زيادة المخالفات الجديدة
4. تحويل القواعد المهمّة فقط إلى Error
5. وضع خطّة لتقليل المخالفات القائمة

غرض الـ Analyzer ليس إزعاج المطوّرين، بل رفع جودة قاعدة الشيفرة دون إرهاق.

30. تصحيح أخطاء Source Generator

بما أنّ Source Generator يعمل في مكان مختلف عن التطبيق العاديّ، فإنّ تصحيح أخطائه يحمل بعض الخصوصيّة.

بشكل أساسيّ، يُحقَّق فيه بهذه الطرق:

النظر إلى المصدر المولَّد
إصدار Diagnostic
كتابة اختبارات
إرفاق مصحِّح أخطاء (debugger) عند الحاجة

في المشاريع بنمط SDK، يسهُل التحقّق باستخدام إعداد إخراج الملفّات المولَّدة.

<PropertyGroup>
  <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
  <CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>

بهذا، يسهُل التحقّق من ملفّات .g.cs المولَّدة.

$(BaseIntermediateOutputPath) يشير عادةً إلى ما تحت obj/.

إن حدَّدتَ مساراً مثل Generated مباشرةً تحت المشروع، فبما أنّ المشاريع بنمط SDK تُدرِج افتراضيّاً **/*.cs كهدف تصريف، فقد يُعاد إدراج ملفّات .g.cs المولَّدة كشيفرة مصدريّة عاديّة في البناء التالي، ما يسبِّب خطأ تكرار في الأنواع أو الأعضاء.

إن كان لا بدّ من الإخراج مباشرةً تحت المشروع، استبعده صراحةً من هدف التصريف عبر شيء مثل <Compile Remove="Generated/**/*.cs" />.

في اختبار Generator، يُستخدَم غالباً شكل مقارنة الشيفرة المُدخَلة بنتيجة التوليد.

تجهيز الشيفرة المصدريّة المُدخَلة
تشغيل Generator
التحقّق من الشيفرة المصدريّة المولَّدة
التحقّق من Diagnostic المتوقَّع

يتعطّل Source Generator سريعاً إن اكتُفي بالتحقّق اليدويّ فقط. كلّما ازداد منطق التوليد تعقيداً، ازدادت أهميّة الاختبارات.

31. الاختبار باستخدام Roslyn

يجب كتابة اختبارات لـ Analyzer وSource Generator وتطويرهما تدريجيّاً. خصوصاً الـ Analyzer، حيث يشكِّل كلّ من الاكتشاف الخاطئ والاكتشاف المفقود مشكلة.

في الاختبار، تُجهَّز أنماط كهذه:

شيفرة يجب اكتشافها
شيفرة يجب ألّا تُكتشَف
شيفرة تستخدم using alias
شيفرة تستخدم اسماً مؤهَّلاً بالكامل (fully qualified name)
شيفرة تستخدم نوعاً آخر بنفس الاسم تقريباً
شيفرة تُعامَل كشيفرة مولَّدة (generated code)
شيفرة عند تفعيل nullable

على سبيل المثال، بالنسبة لـ Analyzer يحظر System.DateTime.Now، تُتحقَّق حالات كهذه:

// ينبغي اكتشافه
var x = System.DateTime.Now;
// ينبغي اكتشافه أيضاً عند وجود using
using System;
var x = DateTime.Now;
// لا ينبغي اكتشافه إن كان نوعاً مختلفاً
namespace MyCompany;

public static class DateTime
{
    public static string Now => "now";
}

var x = DateTime.Now;

هذه الحالة الأخيرة مثال يسهُل الخطأ فيه عند البحث النصّي. في Analyzer الخاصّ بـ Roslyn، يمكن تجنّبه بالتحقّق من الرمز (symbol) المستهدَف عبر SemanticModel.

32. هل يمكن استخدامه في مشاريع .NET Framework أيضاً؟

Roslyn ليس حكراً على .NET الحاليّ. لكن تختلف نقاط الانتباه باختلاف «طريقة الاستخدام».

عند الاستخدام كأداة تحقيق

بناء أداة Roslyn كتطبيق سطر أوامر لـ .NET 8 أو .NET 10، وقراءة حلّ (solution) خاصّ بـ .NET Framework وتحليله، خيار واقعيّ.

في هذه الحالة، يمكن تشغيل الأداة نفسها على .NET الحاليّ، بينما يكون هدف التحليل شيفرة .NET Framework.

لكن عند قراءة الحلّ عبر MSBuildWorkspace، يلزم توفّر MSBuild، وSDK، وأسمبليات مرجعيّة، وبيئة استعادة NuGet قادرة على بناء المشروع الهدف.

بعبارة أخرى، لا يمكن لـ Roslyn وحده قراءة أيّ شيء، بل يلزم بيئة بناء لحلّ بنية المشروع الفعليّة.

عند الاستخدام كـ Analyzer

يعمل الـ Analyzer عند تحميله من قِبَل المُصرِّف أو بيئة التطوير.

حتّى لو كان المشروع الهدف .NET Framework، يمكن استخدامه إن كانت البيئة قادرة على تحميل المُصرِّف للـ Analyzer.

لكن مع csproj قديم، وVisual Studio قديم، وMSBuild قديم، وبنية معتمِدة على packages.config، قد لا يكون التبنّي والتشغيل مباشراً كما هو الحال مع نمط SDK الحاليّ.

عند الإدخال في مشروع .NET Framework قائم، من الأفضل التحقّق من هذه النقاط أوّلاً:

إصدار Visual Studio / MSBuild
هل يمكن استخدام PackageReference
هل يعمل نفس الـ Analyzer في CI
هل تظهر التحذيرات في سجلّ البناء
هل يعمل .editorconfig

عند الاستخدام كـ Source Generator

الـ Source Generator آليّة يحمِّلها المُصرِّف وقت التصريف.

لذلك، فإنّ حالة دعم المُصرِّف أو SDK المُستخدَم في البناء أهمّ من إطار عمل التشغيل الخاصّ بالمشروع الهدف.

في مشاريع نمط SDK لـ .NET الحاليّ، يسهُل التعامل معه، بينما في مشاريع .NET Framework القديمة، يلزم الانتباه حسب صيغة المشروع وبيئة البناء.

بالنسبة للأصول القائمة على .NET Framework، غالباً ما يكون أسلم البدء بأداة تحقيق أو Analyzer قائم على Roslyn، بدل إدخال Source Generator من البداية.

33. ملاحظات حول اختيار الإصدار

تتضمّن حزم NuGet المتعلِّقة بـ Roslyn حزم Microsoft.CodeAnalysis.*.

من الحزم التمثيليّة:

Microsoft.CodeAnalysis.CSharp
Microsoft.CodeAnalysis.CSharp.Workspaces
Microsoft.CodeAnalysis.Workspaces.MSBuild
Microsoft.CodeAnalysis.Analyzers
Microsoft.CodeAnalysis.CSharp.CodeFix.Testing
Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing

النقطة الواجب الانتباه إليها هنا هي أنّ Analyzer وSource Generator يُحمَّلان من قِبَل مُصرِّف المستخدِم نفسه.

بعبارة أخرى، إن كان SDK / Visual Studio لدى المطوّر أو CI قديماً، فقد لا يعمل Analyzer / Generator الذي يستخدم واجهة Roslyn API أحدث ممّا ينبغي.

إن كان بإمكانك توحيد بيئة البناء لاستخدام داخليّ فقط، فمن السهل استخدام واجهات API أحدث نسبيّاً.

من ناحية أخرى، في المكتبات الموزَّعة خارجيّاً، يلزم اختيار إصدار Microsoft.CodeAnalysis المُعتمَد عليه بعناية، مع مراعاة نطاق بيئات المستخدِمين الواسع.

من المفيد التفكير كسياسة كالتالي:

استخدام داخليّ: استخدام واجهات API حديثة نسبيّاً بعد توحيد CI وبيئة التطوير
توزيع خارجيّ: الاختيار بحذر مع مراعاة نطاق SDK/VS لدى المستخدِمين
Generator: تصميمه كـ Incremental Generator إن أمكن
Analyzer: إعطاء الأولويّة للخفّة التي لا تُفسِد تجربة بيئة التطوير

Roslyn قريب من مجال المُصرِّف، لذا فهو عرضة للتأثّر بفروق الإصدارات.

34. لا تحاول فعل كلّ شيء بـ Roslyn

Roslyn قويّ، لكنّه ليس أداة تحلّ كلّ المشكلات. على سبيل المثال، هذه المشكلات لا يمكن حلّها بـ Roslyn وحده:

أيّ فرع يُنفَّذ وقت التشغيل
أيّ قيمة تصل في بيانات الإنتاج
التوابع المُستدعاة ديناميكيّاً عبر Reflection
نتيجة التسجيل وقت التشغيل في حاوية DI
المعالجة التي تتغيّر حسب ملفّ الإعدادات
القيمة العائدة من خدمة خارجيّة

Roslyn أداة تتعامل بشكل أساسيّ مع الشيفرة المصدريّة ومعلومات التصريف. إن أردتَ معرفة السلوك وقت التشغيل، فيلزم استخدام وسائل أخرى، مثل الاختبار، والسجلّات (logs)، والتتبّع (trace)، والتنميط (profiling)، وتحليل التفريغ (dump).

لذلك، من المفيد اعتبار دور Roslyn كالتالي:

التعامل بدقّة عالية مع ما يمكن معرفته سكونيّاً (statically)

محاولة حلّ ما لا يُعرَف إلّا ديناميكيّاً بالقوّة عبر Roslyn تؤدّي إلى آليّة معقّدة وغير دقيقة.

35. ترتيب التبنّي

عند البدء باستخدام Roslyn في العمل الفعليّ، يُنصَح بهذا الترتيب:

1. ضبط Analyzer القائم في .NET و.editorconfig
2. كتابة أداة تحقيق صغيرة باستخدام Syntax Tree
3. تجربة حلّ الأنواع باستخدام SemanticModel
4. قراءة الحلّ (solution) عبر MSBuildWorkspace
5. إنشاء Analyzer صغير خاصّ بالفريق
6. إضافة Code Fix عند الحاجة
7. النظر في Source Generator للمواضع التي تكثر فيها الشيفرة النمطيّة

لا حاجة للذهاب مباشرةً إلى Source Generator. في كثير من بيئات العمل، يكون Analyzer وأداة التحقيق أسرع إظهاراً للأثر.

خصوصاً عند كِبَر الأصول القائمة، فإنّ هذا التدفّق واقعيّ:

معرفة الوضع الحاليّ عبر أداة تحقيق
تحويل المشكلات المتكرّرة إلى Analyzer
تحويل ما يمكن إصلاحه بأمان فقط إلى Code Fix
تحويل الشيفرة النمطيّة المتكرّرة الكتابة إلى Generator

Roslyn أداة يمكن استخدامها تدريجيّاً.

36. مثال صغير: حصر استدعاءات التوابع

أخيراً، لننظر إلى استخدام Roslyn بشكل أقرب قليلاً للعمل الفعليّ. هنا، صورة لحصر استدعاءات التوابع داخل حلّ (solution).

using Microsoft.Build.Locator;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.MSBuild;

MSBuildLocator.RegisterDefaults();

using var workspace = MSBuildWorkspace.Create();
var solution = await workspace.OpenSolutionAsync(args[0]);

foreach (var project in solution.Projects)
{
    var compilation = await project.GetCompilationAsync();
    if (compilation is null)
    {
        continue;
    }

    foreach (var document in project.Documents)
    {
        var tree = await document.GetSyntaxTreeAsync();
        if (tree is null)
        {
            continue;
        }

        var root = await tree.GetRootAsync();
        var semanticModel = compilation.GetSemanticModel(tree);

        var invocations = root
            .DescendantNodes()
            .OfType<InvocationExpressionSyntax>();

        foreach (var invocation in invocations)
        {
            var symbol = semanticModel.GetSymbolInfo(invocation).Symbol as IMethodSymbol;
            if (symbol is null)
            {
                continue;
            }

            var lineSpan = invocation.GetLocation().GetLineSpan();
            var line = lineSpan.StartLinePosition.Line + 1;

            Console.WriteLine(string.Join(",", new[]
            {
                project.Name,
                document.FilePath ?? document.Name,
                line.ToString(),
                symbol.ContainingType.ToDisplayString(),
                symbol.Name
            }));
        }
    }
}

بتوسيع أداة كهذه قليلاً، يمكن إجراء تحقيقات كهذه:

استخراج استدعاءات تابع معيّن فقط
إخراج مواضع استخدام واجهة API مهجورة
إخراج تكرار الاستخدام لكلّ مشروع
إنشاء قائمة واجهات API المُستهدَفة للانتقال

عندما تستطيع قراءة الشيفرة المصدريّة من منظور المُصرِّف، يصبح تحقيق الشيفرة القائمة أسهل بكثير.

37. ملاحظات عند إعادة كتابة الشيفرة باستخدام Roslyn

يمكن أيضاً إعادة كتابة الشيفرة باستخدام شجرة البنية النحويّة في Roslyn.

على سبيل المثال، يمكن تغيير اسم تابع معيّن، أو إضافة سمة (attribute)، أو إضافة using.

لكن يجب إجراء إعادة الكتابة بحذر. نقاط الانتباه هي:

التأكّد من عدم تغيّر المعنى
عدم إفساد التعليقات والمسافات
عدم كبر الفروقات (diff) أكثر من اللازم
توحيد التنسيق
عدم إجراء عدد كبير من التحويلات دفعةً واحدة
تسهيل مراجعة فروقات Git

بما أنّ Syntax Tree في Roslyn يحتفظ بالـ Trivia، فمن الممكن إجراء التحويل مع الحفاظ على التعليقات والمسافات. لكن إن أُنشئت العُقَد (nodes) بإهمال، فقد يفسد تنسيق الشيفرة المولَّدة.

عند بناء أداة إعادة كتابة، هذه السياسة أسلم:

إجراء الاكتشاف فقط أوّلاً
التحقّق من الفروقات قبل التحويل وبعده
البدء بتحويلات صغيرة
كتابة اختبارات لأداة التحويل نفسها
البدء بوضع الاكتشاف فقط في CI

في التحويلات الآليّة واسعة النطاق، Roslyn قويّ، لكن مراجعة الإنسان ضروريّة في النهاية.

38. Roslyn ومساعدة الذكاء الاصطناعيّ في البرمجة

أصبح توليد الشيفرة ودعم المراجعة عبر الذكاء الاصطناعيّ شائعاً في السنوات الأخيرة، لكنّ قيمة Roslyn لا تتراجع وسط ذلك. الذكاء الاصطناعيّ بارع في التعامل مع اللغة الطبيعيّة والسياق المحيط، وRoslyn بارع في التعامل مع معلومات نحويّة ودلاليّة دقيقة كمُصرِّف. العلاقة بينهما تكامليّة أكثر منها تنافسيّة.

على سبيل المثال، يمكن تصوّر توزيع كهذا للأدوار:

استخراج الموضع المستهدَف بدقّة عبر Roslyn
توليد سياسة الإصلاح أو نصّ الشرح عبر الذكاء الاصطناعيّ
التحقّق من قابليّة تصريف مقترح الإصلاح عبر Roslyn
منع التكرار مستقبلاً عبر Analyzer

قد يكون استخراج المواضع المستهدَفة بدقّة عبر Roslyn أكثر أماناً من الطلب من الذكاء الاصطناعيّ «أصلح جميع واجهات API القديمة في قاعدة الشيفرة هذه».

واستخدام الذكاء الاصطناعيّ في دراسة سياسة الإصلاح والمساعدة في المراجعة أكثر عمليّة.

ما يعرفه المُصرِّف، اتركه للمُصرِّف. وليركِّز الإنسان أو الذكاء الاصطناعيّ على الحكم الذي فوق ذلك.

هذا التوزيع مهمّ.

39. قائمة تحقّق للعمل الفعليّ

قبل استخدام Roslyn، من المفيد التحقّق من هذه النقاط:

هل الغرض تحقيق، أم تحذير، أم إصلاح، أم توليد
هل تكفي البنية النحويّة وحدها، أم يلزم التحليل الدلاليّ
هل يكفي ملفّ واحد، أم يلزم المشروع بأكمله
هل يلزم التشغيل داخل بيئة التطوير، أم تكفي أداة مرّة واحدة
هل يجوز التأثير على زمن البناء
هل سيُنفَّذ في CI
هل ستظهر تحذيرات كثيرة جدّاً في الشيفرة القائمة
كيف سيكون مستوى خطورة Analyzer
هل يمكن تطبيق Code Fix بأمان
هل يمكن التحقّق من الشيفرة المولَّدة عبر Source Generator
هل SDK / إصدار Visual Studio لدى المستخدِمين متّسق

عند التردّد في القرار، يُفضَّل التقسيم كالتالي:

أريد التحقيق              -> أداة سطر أوامر تستخدم Roslyn
أريد التطبيق الدائم       -> Analyzer
طريقة الإصلاح محدَّدة       -> Code Fix
أريد إنشاء شيفرة نمطيّة    -> Source Generator

بهذا التقسيم، يقلّ الخطأ في تحديد موضع استخدام Roslyn.

40. الخلاصة

Roslyn هو ما يفتح مُصرِّف C# وVisual Basic كواجهة API يستطيع المطوّرون استخدامها.

باستخدام Roslyn، يمكن التعامل مع الشيفرة المصدريّة ليس كمجرَّد نصّ، بل بهذه الأشكال:

قراءة البنية النحويّة كـ Syntax Tree
قراءة المعنى كـ SemanticModel
التعامل مع التصريف بأكمله كـ Compilation
التعامل مع الحلّ (solution) أو المشروع كـ Workspace
إصدار تحذيرات كـ Analyzer
تقديم مقترحات إصلاح كـ Code Fix
توليد شيفرة كـ Source Generator

في العمل الفعليّ، يفيد بشكل خاصّ في مواقف كهذه:

تحقيق قاعدة الشيفرة القائمة
دعم الانتقال من .NET Framework إلى .NET
التحقّق الآليّ من قواعد الفريق
إرشاد مستخدِمي المكتبة
توليد الشيفرة النمطيّة
ضمان الجودة في بيئة التطوير أو CI

المهمّ هو عدم النظر إلى Roslyn بوصفه «تقنيّة مُصرِّف صعبة» أكثر من اللازم.

في البداية، يكفي قراءة ملفّ واحد عبر CSharpSyntaxTree.ParseText وتعداد أسماء التوابع. من هناك، يمكن التوسّع تدريجيّاً نحو SemanticModel، وWorkspace، وAnalyzer، وSource Generator.

لو أردنا اختصار Roslyn في جملة واحدة، لكانت هذه:

جعل شيفرة C# قابلة للتعامل معها كبنية فهمها المُصرِّف، لا كمجرَّد نصّ.

باعتماد هذا المنظور، تصبح أتمتة مراجعة الشيفرة، والانتقال، والتحقيق، والتوليد أسهل بخطوة إضافيّة.

مراجع

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

ما هو Roslyn؟
Roslyn، واسمه الرسميّ .NET Compiler Platform، هو تطبيق (implementation) لمُصرِّف C# وVisual Basic، وفي الوقت نفسه مجموعة من واجهات API لبناء أدوات تحليل الشيفرة. كان المُصرِّف تقليديّاً «صندوقاً أسود»، لكنّ Roslyn يتيح للتطبيقات والأدوات الوصول إلى المعلومات التي يُنشئها المُصرِّف داخليّاً (مثل: أيّ نوع يشير إليه هذا المعرِّف، وأيّ تابع (method) يشير إليه هذا الاستدعاء، وما إلى ذلك). بذلك، يصبح بالإمكان قراءة شيفرة C# كبنية نحويّة لا كسلسلة نصوص، وقراءتها من حيث المعنى لا من حيث الشكل، وإصدار تحذيرات ومقترحات إصلاح وشيفرة مولَّدة.
ماذا يمكن فعله باستخدام Roslyn؟
يمكن إجراء التحليل النحويّ (syntax) لـ C# وVB، والتحليل الدلاليّ (semantic) للأنواع والتوابع، وتحليل المشروع أو الحلّ (solution) بأكمله، وإنشاء Analyzer وCode Fix وSource Generator خاصّة بك، بالإضافة إلى توليد الشيفرة وتحويلها. بصياغة أقرب للعمل الفعليّ: تحويل استخدام واجهة API محظورة إلى تحذير بناء (build warning)، وحصر مواضع استخدام واجهة API قديمة، وتوليد شيفرة تعيين (mapping) لكائنات DTO وقت التصريف (compile time)، والمساعدة في تحقيق الانتقال من .NET Framework إلى .NET. تُقسَّم طرق الاستخدام إجمالاً إلى أربعة: الاستخدام كمكتبة، وAnalyzer، وCode Fix، وSource Generator.
ما الفرق بينه وبين البحث في الشيفرة عبر التعبيرات النمطيّة (regular expressions) أو grep؟
يصعب على التعبيرات النمطيّة التعامل الصحيح مع النصوص داخل التعليقات، أو النصوص الحرفيّة (string literals)، أو الاستدعاءات المقسَّمة على عدّة أسطر، أو الاستدعاءات عبر اسم مستعار (alias) باستخدام using. باستخدام Roslyn، يمكن التمييز بين التعليقات، والنصوص الحرفيّة، واستدعاءات التوابع على المستوى النحويّ، والتابع المُحلَّل فعليّاً. البحث النصّي البسيط قد يكفي أحياناً للبحث التقريبيّ، لكن عند اتّخاذ قرارات تصميم أو إجراء إصلاحات آليّة بناءً على النتيجة، فإنّ Roslyn، القادر على الحكم استناداً إلى نتيجة تحليل الأسماء لدى المُصرِّف، أكثر أماناً.
من أين يُفضَّل البدء بتعلّم Roslyn؟
لا حاجة للذهاب مباشرةً إلى Source Generator. يُنصَح بالبدء بضبط الـ Analyzer المُضمَّن في .NET SDK القائم وملفّ .editorconfig، ثمّ كتابة أداة تحقيق صغيرة تقرأ ملفّاً واحداً عبر CSharpSyntaxTree.ParseText وتُعدِّد أسماء التوابع، ثمّ التوسّع تدريجيّاً نحو تحليل الأنواع عبر SemanticModel، وقراءة الحلّ (solution) عبر MSBuildWorkspace، وأخيراً إنشاء Analyzer صغير خاصّ بالفريق. في كثير من بيئات العمل، يكون إظهار الأثر أسرع عبر Analyzer وأدوات التحقيق قبل Source Generator.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

روابط عامة

العودة إلى المدونة