كود الجيل مع روسلين

من وقت لآخر ، عندما قرأت عن روسلين ومحلليها ، كنت أفكر دائمًا: "لكن يمكنك أن تصنع هذا الشيء ، الذي سيتجاوز الشفرة ويقوم بتوليد الكود". البحث السريع لم يظهر أي شيء مثير للاهتمام ، لذلك تقرر الحفر. كم كنت متفاجئًا عندما اكتشفت أن فكرتي لم تكن قابلة للتحقيق فقط ، ولكن كل هذا سوف ينجح تقريبًا بدون عكازين.


ومن يهمه الأمر هو أن ينظر إلى كيف يمكنك عمل "انعكاس صغير" وتعبئته في nuget ، من فضلك ، تحت القط.


مقدمة


أعتقد أن أول شيء يجب توضيحه هو المقصود بعبارة "التفكير القليل". أقترح تطبيق لجميع أنواع Dictionary<string, Type> GetBakedType() ، والذي سيعود بأسماء الخصائص وأنواعها. نظرًا لأن هذا يجب أن يعمل مع جميع الأنواع ، فإن الخيار الأسهل سيكون إنشاء طريقة تمديد لكل نوع. سيتضمن التنفيذ اليدوي النموذج التالي:


 using System; using System.Collections.Generic; public static class testSimpleReflectionUserExtentions { private static Dictionary<string, Type> properties = new Dictionary<string, Type> { { "Id", typeof(System.Guid)}, { "FirstName", typeof(string)}, { "LastName", typeof(string)}, }; public static Dictionary<string, Type> GetBakedType(this global::testSimpleReflection.User value) { return properties; } } 

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


"لتمكين" هذه الوظيفة ، نحتاج فقط إلى تثبيت حزمة nuget واحدة وسيعمل كل شيء على الفور. بعد ذلك ، ما GetBakedType سوى الاتصال بـ GetBakedType عند الضرورة ، حيث حصلنا على خطأ في GetBakedType البرمجي يقول إن الانعكاس لهذا النوع ليس جاهزًا بعد ، اتصل بالكوديفيكس وانتهى الأمر. لدينا طريقة تمديد من شأنها أن تعيد جميع الممتلكات العامة إلينا.


أعتقد في الحركة أنه سيكون من الأسهل فهم كيف تعمل بشكل عام ، وهنا تصور قصير:



إذا كنت مهتمًا بتجربة ذلك محليًا ، يمكنك تثبيت حزمة nuget تحت اسم SimpleReflection:


 Install-Package SimpleReflection 

من يهتم بالمصدر ، هم هنا .


أريد أن أحذر من أن هذا التطبيق غير مصمم للاستخدام الحقيقي. أريد فقط أن أظهر طريقة لتنظيم توليد الشفرات مع Roslyn.


التحضير الأولي


قبل أن تبدأ في صنع محللاتك ، يجب عليك تثبيت المكون "تطوير إمتداد Visual Studio" في مثبّت الاستوديو. بالنسبة إلى الإصدار VS 2019 ، يجب أن تتذكر تحديد ".NET Compiler Platform SDK" كمكون اختياري.


تنفيذ محلل


لن أصف على مراحل كيفية تنفيذ المحلل ، لأنه بسيط للغاية ، لكن فقط تابع النقاط الرئيسية.


وستكون النقطة الرئيسية الأولى هي أنه إذا كان لدينا خطأ حقيقي في الترجمة ، فلن يبدأ المحللون على الإطلاق. نتيجة لذلك ، إذا حاولنا استدعاء GetBakedType() في سياق النوع الذي لم يتم تنفيذه من GetBakedType() ولن تكون جميع جهودنا منطقية. ولكن هنا سوف يساعدنا معرفة الأولوية التي يدعو إليها المترجم أساليب التمديد. بيت القصيد هو أن تطبيقات محددة لها الأسبقية على الأساليب العامة. وهذا هو ، في المثال التالي ، سيتم استدعاء الطريقة الثانية ، وليس الأولى:


 public static class SomeExtentions { public static void Save<T>(this T value) { ... } public static void Save(this User user) { ... } } public class Program { public static void Main(string[] args) { var user = new User(); user.Save(); } } 

هذه الميزة مفيدة جدا. نحن ببساطة تعريف GetBakedType العام كما يلي:


 using System; using System.Collections.Generic; public static class StubExtention { public static Dictionary<string, Type> GetBakedType<TValue>(this TValue value) { return new Dictionary<string, Type>(); } } 

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


النظر في محلل نفسه. سيقدم تشخيصين. الأول مسؤول عن الحالة عندما لا يبدأ إنشاء الشفرة على الإطلاق ، والثاني عندما نحتاج إلى تحديث رمز موجود. سيكون لديهم الأسماء التالية SimpleReflectionIsNotReady و SimpleReflectionUpdate على التوالي. ستنشئ التشخيصات الأولى "خطأ في الترجمة" ، أما الثاني فيبلغ فقط أنه يمكنك تشغيل توليد الشفرة مرة أخرى.


وصف التشخيص كالتالي:


  public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); 

بعد ذلك ، من الضروري تحديد ما الذي سنبحث عنه ، وفي هذه الحالة ستكون استدعاءًا للطريقة:


 public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } 

ثم بالفعل في HandelBuilder تحليل شجرة بناء الجملة. عند المدخل ، سوف نتلقى جميع المكالمات التي تم العثور عليها ، لذلك تحتاج إلى التخلص من كل شيء ما عدا GetBakedType لدينا. يمكن القيام بذلك بالطريقة المعتادة if تحقق من اسم الطريقة. بعد ذلك نحصل على نوع المتغير الذي يتم من خلاله تسمية طريقتنا وإبلاغ المترجم بنتائج تحليلنا. قد يكون هذا خطأً في الترجمة إذا لم يبدأ إنشاء الشفرة بعد أو القدرة على إعادة تشغيله.


كل شيء يبدو مثل هذا:


 private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType") { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } 

رمز محلل كامل
  [DiagnosticAnalyzer(LanguageNames.CSharp)] public class SimpleReflectionAnalyzer : DiagnosticAnalyzer { public const string SimpleReflectionIsNotReady = "SimpleReflectionIsNotReady"; public const string SimpleReflectionUpdate = "SimpleReflectionUpdate"; public static DiagnosticDescriptor SimpleReflectionIsNotReadyDescriptor = new DiagnosticDescriptor( SimpleReflectionIsNotReady, "Simple reflection is not ready.", "Simple reflection is not ready.", "Codegen", DiagnosticSeverity.Error, isEnabledByDefault: true, "Simple reflection is not ready."); public static DiagnosticDescriptor SimpleReflectionUpdateDescriptor = new DiagnosticDescriptor( SimpleReflectionUpdate, "Simple reflection update.", "Simple reflection update.", "Codegen", DiagnosticSeverity.Info, isEnabledByDefault: true, "Simple reflection update."); public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(SimpleReflectionIsNotReadyDescriptor, SimpleReflectionUpdateDescriptor); public override void Initialize(AnalysisContext context) { context.RegisterOperationAction(this.HandelBuilder, OperationKind.Invocation); } private void HandelBuilder(OperationAnalysisContext context) { if (context.Operation.Syntax is InvocationExpressionSyntax invocation && invocation.Expression is MemberAccessExpressionSyntax memberAccess && memberAccess.Name is IdentifierNameSyntax methodName && methodName.Identifier.ValueText == "GetBakedType" ) { var semanticModel = context.Compilation .GetSemanticModel(invocation.SyntaxTree); var typeInfo = semanticModel .GetSpeculativeTypeInfo(memberAccess.Expression.SpanStart, memberAccess.Expression, SpeculativeBindingOption.BindAsExpression); var diagnosticProperties = ImmutableDictionary<string, string>.Empty.Add("type", typeInfo.Type.ToDisplayString()); if (context.Compilation.GetTypeByMetadataName(typeInfo.Type.GetSimpleReflectionExtentionTypeName()) is INamedTypeSymbol extention) { var updateDiagnostic = Diagnostic.Create(SimpleReflectionUpdateDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(updateDiagnostic); return; } var diagnostic = Diagnostic.Create(SimpleReflectionIsNotReadyDescriptor, methodName.GetLocation(), diagnosticProperties); context.ReportDiagnostic(diagnostic); } } } 

تطبيق مولد الشفرة


سنفعل توليد CodeFixProvider من خلال CodeFixProvider ، وهو مشترك في CodeFixProvider . بادئ ذي بدء ، نحن بحاجة إلى التحقق مما حدث للعثور على محللنا.


يبدو مثل هذا:


 public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } 

كل السحر يحدث داخل CreateFormatterAsync . في ذلك نحصل على وصف كامل للنوع. ثم نبدأ في إنشاء كود وإضافة ملف جديد إلى المشروع.


الحصول على المعلومات وإضافة ملف:


  private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } 

توليد الشفرة الخاصة (أظن أن المحور سوف يكسر الشبكة الفرعية بالكامل):


 private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } 

مولد رمز كاملة
 using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CodeActions; using Microsoft.CodeAnalysis.CodeFixes; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Formatting; using Microsoft.CodeAnalysis.Text; using SimpleReflection.Utils; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace SimpleReflection { [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(SimpleReflectionCodeFixProvider)), Shared] public class SimpleReflectionCodeFixProvider : CodeFixProvider { public sealed override ImmutableArray<string> FixableDiagnosticIds => ImmutableArray.Create(SimpleReflectionAnalyzer.SimpleReflectionIsNotReady, SimpleReflectionAnalyzer.SimpleReflectionUpdate); public sealed override FixAllProvider GetFixAllProvider() { return WellKnownFixAllProviders.BatchFixer; } public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) { var diagnostic = context.Diagnostics.First(); var title = diagnostic.Severity == DiagnosticSeverity.Error ? "Generate simple reflection" : "Recreate simple reflection"; context.RegisterCodeFix( CodeAction.Create( title, createChangedDocument: token => this.CreateFormatterAsync(context, diagnostic, token), equivalenceKey: title), diagnostic); } private async Task<Document> CreateFormatterAsync(CodeFixContext context, Diagnostic diagnostic, CancellationToken token) { var typeName = diagnostic.Properties["type"]; var currentDocument = context.Document; var model = await context.Document.GetSemanticModelAsync(token); var symbol = model.Compilation.GetTypeByMetadataName(typeName); var symbolName = symbol.ToDisplayString(); var rawSource = this.BuildSimpleReflection(symbol); var source = Formatter.Format(SyntaxFactory.ParseSyntaxTree(rawSource).GetRoot(), new AdhocWorkspace()).ToFullString(); var fileName = $"{symbol.GetSimpleReflectionExtentionTypeName()}.cs"; if (context.Document.Project.Documents.FirstOrDefault(o => o.Name == fileName) is Document document) { return document.WithText(SourceText.From(source)); } var folders = new[] { "SimpeReflection" }; return currentDocument.Project .AddDocument(fileName, source) .WithFolders(folders); } private string BuildSimpleReflection(INamedTypeSymbol symbol) => $@" using System; using System.Collections.Generic; // Simple reflection for {symbol.ToDisplayString()} public static class {symbol.GetSimpleReflectionExtentionTypeName()} {{ private static Dictionary<string, Type> properties = new Dictionary<string, Type> {{ { symbol .GetAllMembers() .OfType<IPropertySymbol>() .Where(o => (o.DeclaredAccessibility & Accessibility.Public) > 0) .Select(o => $@" {{ ""{o.Name}"", typeof({o.Type.ToDisplayString()})}},") .JoinWithNewLine() } }}; public static Dictionary<string, Type> GetBakedType(this global::{symbol.ToDisplayString()} value) {{ return properties; }} }} "; } } 

النتائج


نتيجة لذلك ، لدينا منشئ رمز محلل Roslyn مع مساعدة منه يتم تنفيذ انعكاس "صغير" باستخدام إنشاء التعليمات البرمجية. سيكون من الصعب التوصل إلى تطبيق حقيقي للمكتبة الحالية ، ولكنه سيكون مثالاً رائعًا على تطبيق منشئي الشفرات يمكن الوصول إليهم بسهولة. يمكن أن يكون هذا النهج مفيدًا لكتابة المتسلسلين ، مثل أي إنشاء رمز. لقد نجح تنفيذ اختبار MessagePack الخاص بي بنسبة 20٪ أسرع من تطبيق neuecc / MessagePack-CSharp ، ولم أرَ مُسلسل أسرع. بالإضافة إلى ذلك ، هذا النهج لا يتطلب Roslyn.Emit ، وهو مثالي لسيناريوهات الوحدة والوحدة.

Source: https://habr.com/ru/post/ar455952/


All Articles